mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
update views using skeletons and component helpers
This commit is contained in:
parent
0486865f56
commit
6f028961c0
40 changed files with 1843 additions and 2303 deletions
|
@ -51,11 +51,9 @@ import { mapGetters } from 'vuex'
|
|||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import AdressInputSelect from '@/components/AdressInputSelect'
|
||||
|
||||
import { formatFormDataValue } from '@/helpers/yunohostArguments'
|
||||
import { required, domain, dynDomain } from '@/helpers/validators'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'DomainForm',
|
||||
|
||||
|
@ -139,12 +137,9 @@ export default {
|
|||
},
|
||||
|
||||
created () {
|
||||
if (this.noStore) return
|
||||
this.$store.dispatch('FETCH', { uri: 'domains' }).then(() => {
|
||||
if (this.dynDnsForbiden) {
|
||||
this.selected = 'domain'
|
||||
}
|
||||
})
|
||||
if (this.dynDnsForbiden) {
|
||||
this.selected = 'domain'
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
|
|
@ -5,14 +5,19 @@
|
|||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<b-alert variant="warning" show>
|
||||
<p class="alert alert-warning">
|
||||
{{ $t('good_practices_about_admin_password') }}
|
||||
</b-alert>
|
||||
</p>
|
||||
<slot name="disclaimer" />
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
<slot name="extra-fields" v-bind="{ v: $v, fields, form }" />
|
||||
<slot name="extra" v-bind="{ v: $v, fields, form }">
|
||||
<form-field
|
||||
v-for="(value, key) in extra.fields" :key="key"
|
||||
v-bind="value" v-model="$v.form.$model[key]" :validation="$v.form[key]"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<!-- ADMIN PASSWORD -->
|
||||
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"backups_no": "No backup",
|
||||
"begin": "Begin",
|
||||
"both": "Both",
|
||||
"cancel": "Cancel",
|
||||
"catalog": "Catalog",
|
||||
"check": "Check",
|
||||
"close": "Close",
|
||||
|
@ -115,6 +116,7 @@
|
|||
"diagnosis_explanation": "The diagnosis feature will attempt to identify common issues on the different aspects of your server to make sure everything runs smoothly. The diagnosis runs automatically twice a day and an email is sent to the administrator if issues are found. Note that some tests may not be relevant if you do not want to use some specific features (for example XMPP) or may fail if you have a complex setup. In such cases, and if you know what you are doing, it is alright to ignore the corresponding issues or warnings.",
|
||||
"run_first_diagnosis": "Run initial diagnosis",
|
||||
"disable": "Disable",
|
||||
"disabled": "Disabled",
|
||||
"dns": "DNS",
|
||||
"domain_add": "Add domain",
|
||||
"domain_add_dns_doc": "… and I have <a href='//yunohost.org/dns'>set my DNS correctly</a>.",
|
||||
|
@ -124,6 +126,7 @@
|
|||
"domain_default_desc": "The default domain is the connection domain where users log in.",
|
||||
"domain_default_longdesc": "This is your default domain.",
|
||||
"domain_delete_longdesc": "Delete this domain",
|
||||
"domain_delete_forbidden_desc": "You cannot remove '{domain}' since it's the default domain, you need to choose another domain (or <a href='#/domains/add'>add a new one</a>) and set it as the default domain to be able to remove this one.",
|
||||
"domain_dns_config": "DNS configuration",
|
||||
"domain_dns_longdesc": "View DNS configuration",
|
||||
"domain_name": "Domain name",
|
||||
|
@ -132,6 +135,7 @@
|
|||
"domains": "Domains",
|
||||
"download": "Download",
|
||||
"enable": "Enable",
|
||||
"enabled": "Enabled",
|
||||
"error": "Error",
|
||||
"error_modify_something": "You should modify something",
|
||||
"error_server_unexpected": "Unexpected server error",
|
||||
|
@ -149,6 +153,7 @@
|
|||
"form_errors": {
|
||||
"alpha": "Value must be alphabetical characters only.",
|
||||
"alphalownum_": "Value must be lower-case alphanumeric and underscore characters only.",
|
||||
"between": "Value must be between {min} and {max}.",
|
||||
"domain": "Invalid domain name: Must be lower-case alphanumeric, dot and dash characters only",
|
||||
"dynDomain": "Invalid domain name: Must be lower-case alphanumeric and dash characters only",
|
||||
"email": "Invalid email: must be alphanumeric and <code>_.-</code> characters only (e.g. someone@example.com, s0me-1@example.com)",
|
||||
|
@ -370,6 +375,7 @@
|
|||
"unauthorized": "Unauthorized",
|
||||
"unignore": "Unignore",
|
||||
"uninstall": "Uninstall",
|
||||
"unknown": "Unknown",
|
||||
"unmaintained": "Unmaintained",
|
||||
"unmaintained_details": "This app has not been update for quite a while and the previous maintainer has gone away or does not have time to maintain this app. Feel free to check the app repository to provide your help",
|
||||
"upnp": "UPnP",
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
<em v-t="'api_error.sorry'" />
|
||||
|
||||
<b-alert variant="info" class="mt-4" show>
|
||||
<div class="alert alert-info mt-4">
|
||||
<span v-html="$t('api_error.help')" />
|
||||
<br>{{ $t('api_error.info') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
|
||||
<h5 v-t="'error'" />
|
||||
<pre><code>"{{ error.code }}" {{ error.status }}</code></pre>
|
||||
|
@ -36,7 +36,3 @@ export default {
|
|||
// FIXME add redirect if they're no error (if reload or route entered by hand)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<b-list-group class="menu-list">
|
||||
<b-list-group-item
|
||||
v-for="item in menu"
|
||||
:key="item.id"
|
||||
:to="{name: item.routeName}"
|
||||
:key="item.routeName"
|
||||
:to="{ name: item.routeName }"
|
||||
>
|
||||
<icon :iname="item.icon" class="lg" />
|
||||
<h2>{{ $t(item.translation) }}</h2>
|
||||
|
@ -18,17 +18,17 @@
|
|||
export default {
|
||||
name: 'Home',
|
||||
|
||||
data: () => {
|
||||
data () {
|
||||
return {
|
||||
menu: [
|
||||
{ id: 0, routeName: 'user-list', icon: 'users', translation: 'users' },
|
||||
{ id: 1, routeName: 'domain-list', icon: 'globe', translation: 'domains' },
|
||||
{ id: 2, routeName: 'app-list', icon: 'cubes', translation: 'applications' },
|
||||
{ id: 3, routeName: 'update', icon: 'refresh', translation: 'system_update' },
|
||||
{ id: 4, routeName: 'service-list', icon: 'cog', translation: 'services' },
|
||||
{ id: 5, routeName: 'tool-list', icon: 'wrench', translation: 'tools' },
|
||||
{ id: 6, routeName: 'diagnosis', icon: 'stethoscope', translation: 'diagnosis' },
|
||||
{ id: 7, routeName: 'backup', icon: 'archive', translation: 'backup' }
|
||||
{ routeName: 'user-list', icon: 'users', translation: 'users' },
|
||||
{ routeName: 'domain-list', icon: 'globe', translation: 'domains' },
|
||||
{ routeName: 'app-list', icon: 'cubes', translation: 'applications' },
|
||||
{ routeName: 'update', icon: 'refresh', translation: 'system_update' },
|
||||
{ routeName: 'service-list', icon: 'cog', translation: 'services' },
|
||||
{ routeName: 'tool-list', icon: 'wrench', translation: 'tools' },
|
||||
{ routeName: 'diagnosis', icon: 'stethoscope', translation: 'diagnosis' },
|
||||
{ routeName: 'backup', icon: 'archive', translation: 'backup' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
<b-alert v-if="apiError" variant="danger" show>
|
||||
<b-alert v-if="apiError" variant="danger">
|
||||
<icon iname="exclamation-triangle" /> {{ $t(apiError) }}
|
||||
</b-alert>
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
|||
export default {
|
||||
name: 'Login',
|
||||
|
||||
data: () => {
|
||||
data () {
|
||||
return {
|
||||
disabled: false,
|
||||
password: '',
|
||||
|
@ -46,7 +46,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
async login () {
|
||||
login () {
|
||||
this.$store.dispatch('LOGIN', this.password).catch(() => {
|
||||
this.isValid = false
|
||||
})
|
||||
|
@ -66,6 +66,3 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
<div class="post-install">
|
||||
<!-- START STEP -->
|
||||
<template v-if="step === 'start'">
|
||||
<b-alert variant="success" show>
|
||||
<p class="alert alert-success">
|
||||
<icon iname="thumbs-up" /> {{ $t('postinstall_intro_1') }}
|
||||
</b-alert>
|
||||
<b-alert variant="info" show>
|
||||
</p>
|
||||
|
||||
<p class="alert alert-info">
|
||||
<span v-t="'postinstall_intro_2'" />
|
||||
<br>
|
||||
<span v-html="$t('postinstall_intro_3')" />
|
||||
</b-alert>
|
||||
</p>
|
||||
|
||||
<b-button size="lg" variant="primary" @click="step = 'domain'">
|
||||
{{ $t('begin') }}
|
||||
</b-button>
|
||||
|
@ -17,12 +19,9 @@
|
|||
|
||||
<!-- DOMAIN SETUP STEP -->
|
||||
<template v-else-if="step === 'domain'">
|
||||
<domain-form
|
||||
:title="$t('postinstall_set_domain')" :submit-text="$t('next')"
|
||||
no-store @submit="setDomain"
|
||||
>
|
||||
<domain-form @submit="setDomain" :title="$t('postinstall_set_domain')" :submit-text="$t('next')">
|
||||
<template #disclaimer>
|
||||
<b-alert variant="warning" show v-t="'postinstall_domain'" />
|
||||
<p class="alert alert-warning" v-t="'postinstall_domain'" />
|
||||
</template>
|
||||
</domain-form>
|
||||
|
||||
|
@ -35,7 +34,7 @@
|
|||
<template v-else-if="step === 'password'">
|
||||
<password-form :title="$t('postinstall_set_password')" :submit-text="$t('next')" @submit="setPassword">
|
||||
<template #disclaimer>
|
||||
<b-alert variant="warning" show v-t="'postinstall_password'" />
|
||||
<p class="alert alert-warning" v-t="'postinstall_password'" />
|
||||
</template>
|
||||
</password-form>
|
||||
|
||||
|
@ -46,9 +45,9 @@
|
|||
|
||||
<!-- POST-INSTALL SUCCESS STEP -->
|
||||
<template v-else-if="step === 'login'">
|
||||
<b-alert variant="success" show>
|
||||
<p class="alert alert-success">
|
||||
<icon iname="thumbs-up" /> {{ $t('installation_complete') }}
|
||||
</b-alert>
|
||||
</p>
|
||||
<login-view />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div class="app-actions">
|
||||
<div v-if="actions">
|
||||
<b-alert variant="warning" show class="mb-4">
|
||||
<view-base
|
||||
:queries="queries" @queries-response="formatAppActions"
|
||||
ref="view" skeleton="card-form-skeleton"
|
||||
>
|
||||
<template v-if="actions" #default>
|
||||
<b-alert variant="warning" class="mb-4">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('experimental_warning') }}
|
||||
</b-alert>
|
||||
|
||||
|
@ -13,9 +16,9 @@
|
|||
@submit.prevent="performAction(action)" :submit-text="$t('perform')"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<b-alert
|
||||
<div
|
||||
v-if="action.formDisclaimer"
|
||||
show variant="info" v-html="action.formDisclaimer"
|
||||
class="alert alert-info" v-html="action.formDisclaimer"
|
||||
/>
|
||||
<b-card-text v-if="action.description" v-html="action.description" />
|
||||
</template>
|
||||
|
@ -25,13 +28,13 @@
|
|||
v-bind="field" v-model="action.form[fname]" :validation="$v.actions[i][fname]"
|
||||
/>
|
||||
</card-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- In case of a custom url with no manifest found -->
|
||||
<b-alert v-else-if="actions === null" variant="warning" show>
|
||||
<b-alert v-else-if="actions === null" variant="warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_no_actions') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -41,7 +44,6 @@ import { validationMixin } from 'vuelidate'
|
|||
import { formatI18nField, formatYunoHostArguments, formatFormData } from '@/helpers/yunohostArguments'
|
||||
import { objectToParams } from '@/helpers/commons'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'AppActions',
|
||||
|
||||
|
@ -51,6 +53,12 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
`apps/${this.id}/actions`,
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
],
|
||||
actions: undefined
|
||||
}
|
||||
},
|
||||
|
@ -66,18 +74,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
Promise.all([
|
||||
api.get(`apps/${this.id}/actions`),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
])
|
||||
]).then((responses) => this.setupForm(responses[0]))
|
||||
},
|
||||
|
||||
setupForm (data) {
|
||||
formatAppActions (data) {
|
||||
if (!data.actions) {
|
||||
this.actions = null
|
||||
return
|
||||
|
@ -102,17 +99,13 @@ export default {
|
|||
const args = objectToParams(action.form ? formatFormData(action.form) : { wut: undefined })
|
||||
|
||||
api.put(`apps/${this.id}/actions/${action.id}`, { args }).then(response => {
|
||||
this.fetchData()
|
||||
this.$refs.view.fetchQueries()
|
||||
}).catch(error => {
|
||||
action.serverError = error.message
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
mixins: [validationMixin]
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,44 +1,51 @@
|
|||
<template>
|
||||
<div class="app-catalog" v-if="apps">
|
||||
<!-- APP SEARCH -->
|
||||
<b-input-group>
|
||||
<b-input-group-prepend is-text>
|
||||
<icon iname="search" />
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="search-input" :placeholder="$t('search_for_apps')"
|
||||
v-model="search" @input="setCategory"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-select v-model="quality" :options="qualityOptions" @change="setCategory" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<view-search
|
||||
:items="apps" :filtered-items="filteredApps" items-name="apps"
|
||||
:queries="queries" @queries-response="formatAppData"
|
||||
>
|
||||
<template #top-bar>
|
||||
<div id="view-top-bar">
|
||||
<!-- APP SEARCH -->
|
||||
<b-input-group>
|
||||
<b-input-group-prepend is-text>
|
||||
<icon iname="search" />
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="search-input" :placeholder="$t('search.for', { items: $tc('items.apps', 2) })"
|
||||
v-model="search" @input="setCategory"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-select v-model="quality" :options="qualityOptions" @change="setCategory" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<!-- CATEGORY SELECT -->
|
||||
<b-input-group class="mt-3">
|
||||
<b-input-group-prepend is-text>
|
||||
<icon iname="filter" />
|
||||
</b-input-group-prepend>
|
||||
<b-select v-model="category" :options="categories" />
|
||||
<b-input-group-append>
|
||||
<b-button variant="primary" :disabled="category === null" @click="category = null">
|
||||
{{ $t('app_show_categories') }}
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<!-- CATEGORY SELECT -->
|
||||
<b-input-group class="mt-3">
|
||||
<b-input-group-prepend is-text>
|
||||
<icon iname="filter" />
|
||||
</b-input-group-prepend>
|
||||
<b-select v-model="category" :options="categories" />
|
||||
<b-input-group-append>
|
||||
<b-button variant="primary" :disabled="category === null" @click="category = null">
|
||||
{{ $t('app_show_categories') }}
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<!-- CATEGORIES SUBTAGS -->
|
||||
<b-input-group v-if="subtags" class="mt-3 subtags">
|
||||
<b-input-group-prepend is-text>
|
||||
Subtags
|
||||
</b-input-group-prepend>
|
||||
<b-form-radio-group
|
||||
id="subtags-radio" name="subtags"
|
||||
v-model="subtag" :options="subtags"
|
||||
buttons button-variant="outline-secondary"
|
||||
/>
|
||||
<b-select id="subtags-select" v-model="subtag" :options="subtags" />
|
||||
</b-input-group>
|
||||
<!-- CATEGORIES SUBTAGS -->
|
||||
<b-input-group v-if="subtags" class="mt-3 subtags">
|
||||
<b-input-group-prepend is-text>
|
||||
Subtags
|
||||
</b-input-group-prepend>
|
||||
<b-form-radio-group
|
||||
id="subtags-radio" name="subtags"
|
||||
v-model="subtag" :options="subtags"
|
||||
buttons button-variant="outline-secondary"
|
||||
/>
|
||||
<b-select id="subtags-select" v-model="subtag" :options="subtags" />
|
||||
</b-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CATEGORIES CARDS -->
|
||||
<b-card-group v-if="category === null" deck>
|
||||
|
@ -56,7 +63,7 @@
|
|||
</b-card-group>
|
||||
|
||||
<!-- APPS CARDS -->
|
||||
<b-card-group v-else-if="filteredApps.length > 0" deck>
|
||||
<b-card-group v-else deck>
|
||||
<b-card no-body v-for="app in filteredApps" :key="app.id">
|
||||
<b-card-body class="d-flex flex-column">
|
||||
<b-card-title class="d-flex">
|
||||
|
@ -90,7 +97,7 @@
|
|||
<icon iname="book" /> {{ $t('readme') }}
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="app.isInstallable" :variant="app.color" @click="onAppInstallClick(app)">
|
||||
<b-button v-if="app.isInstallable" :variant="app.color" @click="onInstallClick(app)">
|
||||
<icon iname="plus" /> {{ $t('install') }} <icon v-if="app.color === 'danger'" class="ml-1" iname="warning" />
|
||||
</b-button>
|
||||
<b-button v-else :variant="app.color" disabled>
|
||||
|
@ -100,88 +107,82 @@
|
|||
</b-card>
|
||||
</b-card-group>
|
||||
|
||||
<!-- NO APPS -->
|
||||
<b-alert
|
||||
v-else
|
||||
variant="warning" show class="mt-4"
|
||||
>
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_not_found') }}
|
||||
</b-alert>
|
||||
|
||||
<!-- INSTALL CUSTOM APP -->
|
||||
<card-form
|
||||
:title="$t('custom_app_install')" icon="download"
|
||||
@submit.prevent="$refs['custom-app-install-modal'].show()" :submit-text="$t('install')"
|
||||
:validation="$v" class="mt-5"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<b-alert variant="warning" show>
|
||||
<icon iname="exclamation-triangle" /> {{ $t('confirm_install_custom_app') }}
|
||||
</b-alert>
|
||||
<template #bot>
|
||||
<!-- INSTALL CUSTOM APP -->
|
||||
<card-form
|
||||
:title="$t('custom_app_install')" icon="download"
|
||||
@submit.prevent="onCustomInstallClick" :submit-text="$t('install')"
|
||||
:validation="$v" class="mt-5"
|
||||
>
|
||||
<template #disclaimer>
|
||||
<div class="alert alert-warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('confirm_install_custom_app') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- URL -->
|
||||
<form-field v-bind="customInstall.field" v-model="customInstall.url" :validation="$v.customInstall.url" />
|
||||
</template>
|
||||
</card-form>
|
||||
</card-form>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- CONFIRM APP INSTALL MODAL -->
|
||||
<b-modal
|
||||
id="app-install-modal" centered ref="app-install-modal"
|
||||
:ok-title="$t('install')" :title="$t('confirm_app_install')"
|
||||
:header-bg-variant="selectedApp.color"
|
||||
:header-text-variant="selectedApp.color === 'danger' ? 'light' : 'dark'"
|
||||
@ok="goToAppInstallForm"
|
||||
>
|
||||
{{ $t('confirm_install_app_' + selectedApp.state) }}
|
||||
</b-modal>
|
||||
|
||||
<!-- CONFIRM CUSTOM APP INSTALL MODAL -->
|
||||
<b-modal
|
||||
id="custom-app-install-modal" :ref="'custom-app-install-modal'" centered
|
||||
:ok-title="$t('install')" :title="$t('confirm_app_install')"
|
||||
header-bg-variant="danger" header-text-variant="light"
|
||||
@ok="goToCustomAppInstallForm"
|
||||
>
|
||||
{{ $t('confirm_install_custom_app') }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<!-- CUSTOM SKELETON -->
|
||||
<template #skeleton>
|
||||
<b-card-group deck>
|
||||
<b-card
|
||||
v-for="i in 15" :key="i"
|
||||
no-body style="min-height: 10rem;"
|
||||
>
|
||||
<div class="d-flex w-100 mt-auto">
|
||||
<b-skeleton width="30px" height="30px" class="mr-2 ml-auto" />
|
||||
<b-skeleton :width="randint(30, 70) + '%'" height="30px" class="mr-auto" />
|
||||
</div>
|
||||
<b-skeleton
|
||||
v-if="randint(0, 1)"
|
||||
:width="randint(30, 85) + '%'" height="24px" class="mx-auto"
|
||||
/>
|
||||
<b-skeleton :width="randint(30, 85) + '%'" height="24px" class="mx-auto mb-auto" />
|
||||
</b-card>
|
||||
</b-card-group>
|
||||
</template>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import api from '@/api'
|
||||
import { required, githubLink } from '@/helpers/validators'
|
||||
|
||||
import { randint } from '@/helpers/commons'
|
||||
|
||||
export default {
|
||||
name: 'AppCatalog',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: ['appscatalog?full&with_categories'],
|
||||
|
||||
// Data
|
||||
apps: undefined,
|
||||
|
||||
// Filtering options
|
||||
qualityOptions: [
|
||||
{ value: 'isHighQuality', text: this.$i18n.t('only_highquality_apps') },
|
||||
{ value: 'isDecentQuality', text: this.$i18n.t('only_decent_quality_apps') },
|
||||
{ value: 'isWorking', text: this.$i18n.t('only_working_apps') },
|
||||
{ value: 'all', text: this.$i18n.t('all_apps') }
|
||||
],
|
||||
// Computed/filled from api data
|
||||
categories: [
|
||||
{ text: this.$i18n.t('app_choose_category'), value: null },
|
||||
{ text: this.$i18n.t('all_apps'), value: 'all', icon: 'search' }
|
||||
// The rest is filled from api data
|
||||
],
|
||||
apps: undefined,
|
||||
|
||||
// Set by user inputs
|
||||
search: '',
|
||||
category: null,
|
||||
subtag: 'all',
|
||||
search: '',
|
||||
quality: 'isDecentQuality',
|
||||
selectedApp: {
|
||||
// Set some basic values to avoid modal errors
|
||||
state: 'lowquality',
|
||||
color: 'warning'
|
||||
},
|
||||
|
||||
// Custom install form
|
||||
customInstall: {
|
||||
field: {
|
||||
|
@ -199,12 +200,13 @@ export default {
|
|||
|
||||
computed: {
|
||||
filteredApps () {
|
||||
if (!this.apps || this.category === null) return
|
||||
const search = this.search.toLowerCase()
|
||||
|
||||
if (this.quality === 'all' && this.category === 'all' && search === '') {
|
||||
return this.apps
|
||||
}
|
||||
return this.apps.filter(app => {
|
||||
const filtered = this.apps.filter(app => {
|
||||
// app doesn't match quality filter
|
||||
if (this.quality !== 'all' && !app[this.quality]) return false
|
||||
// app doesn't match category filter
|
||||
|
@ -220,6 +222,7 @@ export default {
|
|||
if (app.searchValues.includes(search)) return true
|
||||
return false
|
||||
})
|
||||
return filtered.length ? filtered : null
|
||||
},
|
||||
|
||||
subtags () {
|
||||
|
@ -246,33 +249,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get('appscatalog?full&with_categories').then((data) => {
|
||||
// APPS
|
||||
const apps = []
|
||||
for (const key in data.apps) {
|
||||
const app = data.apps[key]
|
||||
if (app.state === 'notworking') continue
|
||||
|
||||
Object.assign(app, this.getQuality(app))
|
||||
app.isInstallable = !app.installed || app.manifest.multi_instance
|
||||
if (app.maintained !== 'request_adoption') {
|
||||
app.maintained = app.maintained ? 'maintained' : 'orphaned'
|
||||
}
|
||||
app.color = this.getColor(app)
|
||||
app.searchValues = [app.id, app.state, app.manifest.name.toLowerCase(), app.manifest.description.toLowerCase()].join(' ')
|
||||
apps.push(app)
|
||||
}
|
||||
this.apps = apps.sort((a, b) => a.id > b.id ? 1 : -1)
|
||||
|
||||
// CATEGORIES
|
||||
data.categories.forEach(({ title, id, icon, subtags, description }) => {
|
||||
this.categories.push({ text: title, value: id, icon, subtags, description })
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
getQuality (app) {
|
||||
formatQuality (app) {
|
||||
const filters = {
|
||||
isHighQuality: false,
|
||||
isDecentQuality: false,
|
||||
|
@ -297,13 +274,37 @@ export default {
|
|||
return filters
|
||||
},
|
||||
|
||||
getColor (app) {
|
||||
formatColor (app) {
|
||||
if (app.isHighQuality) return 'best'
|
||||
if (app.isDecentQuality) return 'success'
|
||||
if (app.isWorking) return 'warning'
|
||||
return 'danger'
|
||||
},
|
||||
|
||||
formatAppData (data) {
|
||||
// APPS
|
||||
const apps = []
|
||||
for (const key in data.apps) {
|
||||
const app = data.apps[key]
|
||||
if (app.state === 'notworking') continue
|
||||
|
||||
Object.assign(app, this.formatQuality(app))
|
||||
app.isInstallable = !app.installed || app.manifest.multi_instance
|
||||
if (app.maintained !== 'request_adoption') {
|
||||
app.maintained = app.maintained ? 'maintained' : 'orphaned'
|
||||
}
|
||||
app.color = this.formatColor(app)
|
||||
app.searchValues = [app.id, app.state, app.manifest.name.toLowerCase(), app.manifest.description.toLowerCase()].join(' ')
|
||||
apps.push(app)
|
||||
}
|
||||
this.apps = apps.sort((a, b) => a.id > b.id ? 1 : -1)
|
||||
|
||||
// CATEGORIES
|
||||
data.categories.forEach(({ title, id, icon, subtags, description }) => {
|
||||
this.categories.push({ text: title, value: id, icon, subtags, description })
|
||||
})
|
||||
},
|
||||
|
||||
setCategory () {
|
||||
// allow search without selecting a category
|
||||
if (this.category === null) {
|
||||
|
@ -311,34 +312,30 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// INSTALL APP METHODS
|
||||
|
||||
onAppInstallClick (app) {
|
||||
this.selectedApp = app
|
||||
// INSTALL APP
|
||||
async onInstallClick (app) {
|
||||
if (!app.isDecentQuality) {
|
||||
// Ask for confirmation
|
||||
this.$refs['app-install-modal'].show()
|
||||
} else {
|
||||
this.goToAppInstallForm()
|
||||
const state = app.color === 'danger' ? 'inprogress' : app.state
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_install_app_' + state))
|
||||
if (!confirmed) return
|
||||
}
|
||||
this.$router.push({ name: 'app-install', params: { id: app.id } })
|
||||
},
|
||||
|
||||
goToAppInstallForm () {
|
||||
this.$router.push({ name: 'app-install', params: { id: this.selectedApp.id } })
|
||||
},
|
||||
// INSTALL CUSTOM APP
|
||||
async onCustomInstallClick () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_install_custom_app'))
|
||||
if (!confirmed) return
|
||||
|
||||
// INSTALL CUSTOM APP METHODS
|
||||
goToCustomAppInstallForm () {
|
||||
const url = this.customInstall.url
|
||||
this.$router.push({
|
||||
name: 'app-install-custom',
|
||||
params: { id: url.endsWith('/') ? url : url + '/' }
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
randint
|
||||
},
|
||||
|
||||
mixins: [validationMixin]
|
||||
|
@ -346,18 +343,37 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#search-input {
|
||||
min-width: 8rem;
|
||||
#view-top-bar {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
#search-input {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.subtags {
|
||||
#subtags-radio {
|
||||
display: none
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
#subtags-radio {
|
||||
display: inline-flex;
|
||||
}
|
||||
#subtags-select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.card-deck {
|
||||
.card {
|
||||
border-color: $gray-400;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-basis: 90%;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-basis: 50%;
|
||||
|
@ -402,29 +418,14 @@ select {
|
|||
.btn:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-dark {
|
||||
border-color: $gray-400;
|
||||
.btn-outline-dark {
|
||||
border-color: $gray-400;
|
||||
|
||||
&:hover {
|
||||
border-color: $dark;
|
||||
}
|
||||
&:hover {
|
||||
border-color: $dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subtags {
|
||||
#subtags-radio {
|
||||
display: none
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
#subtags-radio {
|
||||
display: inline-flex;
|
||||
}
|
||||
#subtags-select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<div class="app-config-panel">
|
||||
<div v-if="panels">
|
||||
<b-alert variant="warning" show class="mb-4">
|
||||
<view-base :queries="queries" @queries-response="formatAppConfig" skeleton="card-form-skeleton">
|
||||
<template v-if="panels" #default>
|
||||
<b-alert variant="warning" class="mb-4">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('experimental_warning') }}
|
||||
</b-alert>
|
||||
|
||||
<!-- FIXME Rework with components -->
|
||||
<b-form id="config-form" @submit.prevent="applyConfig">
|
||||
<b-card no-body v-for="panel in panels" :key="panel.id">
|
||||
<b-card-header class="d-flex align-items-center">
|
||||
|
@ -35,13 +36,13 @@
|
|||
</b-collapse>
|
||||
</b-card>
|
||||
</b-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- if no config panel -->
|
||||
<b-alert v-else-if="panels === null" variant="warning" show>
|
||||
<b-alert v-else-if="panels === null" variant="warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_config_panel_no_panel') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -54,31 +55,23 @@ export default {
|
|||
name: 'AppConfigPanel',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
`apps/${this.id}/config-panel`,
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
],
|
||||
panels: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
Promise.all([
|
||||
api.get(`apps/${this.id}/config-panel`),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
])
|
||||
]).then((responses) => this.setupForm(responses[0]))
|
||||
},
|
||||
|
||||
setupForm (data) {
|
||||
formatAppConfig (data) {
|
||||
if (!data.config_panel || data.config_panel.length === 0) {
|
||||
this.panels = null
|
||||
return
|
||||
|
@ -121,10 +114,6 @@ export default {
|
|||
console.log('ERROR', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
<template>
|
||||
<div class="app-info" v-if="info">
|
||||
<view-base :queries="queries" @queries-response="formatAppData" ref="view">
|
||||
<!-- BASIC INFOS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="info-circle" /> {{ $t('infos') }} — {{ info.label }}</h2>
|
||||
</template>
|
||||
|
||||
<card v-if="infos" :title="`${$t('infos')} — ${infos.label}`" icon="info-circle">
|
||||
<b-row
|
||||
v-for="(value, prop) in info" :key="prop"
|
||||
v-for="(value, prop) in infos" :key="prop"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col cols="auto" md="3">
|
||||
|
@ -33,14 +29,10 @@
|
|||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- OPERATIONS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="wrench" /> {{ $t('operations') }}</h2>
|
||||
</template>
|
||||
|
||||
<card v-if="app" :title="$t('operations')" icon="wrench">
|
||||
<!-- CHANGE PERMISSIONS LABEL -->
|
||||
<b-form-group :label="$t('app_manage_label_and_tiles')" label-class="font-weight-bold">
|
||||
<form-field
|
||||
|
@ -98,16 +90,13 @@
|
|||
<b-input id="input-url" v-model="form.url.path" class="flex-grow-3" />
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="info" v-t="'save'"
|
||||
@click="action = 'changeUrl'" v-b-modal.modal
|
||||
/>
|
||||
<b-button @click="changeUrl" variant="info" v-t="'save'" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<b-alert v-else variant="warning" show>
|
||||
<div v-else class="alert alert-warning">
|
||||
<icon iname="exclamation" /> {{ $t('app_info_change_url_disabled_tooltip') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
|
||||
|
@ -116,14 +105,9 @@
|
|||
:label="$t('app_info_default_desc', { domain: app.domain })" label-for="main-domain"
|
||||
label-class="font-weight-bold" label-cols-md="4"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-button
|
||||
id="main-domain" variant="success" v-b-modal.modal
|
||||
@click="action = 'setAsDefaultDomain'"
|
||||
>
|
||||
<icon iname="star" /> {{ $t('app_make_default') }}
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
<b-button @click="setAsDefaultDomain" id="main-domain" variant="success">
|
||||
<icon iname="star" /> {{ $t('app_make_default') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
|
||||
|
@ -132,56 +116,45 @@
|
|||
:label="$t('app_info_uninstall_desc')" label-for="uninstall"
|
||||
label-class="font-weight-bold" label-cols-md="4"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-button
|
||||
id="uninstall" variant="danger" v-b-modal.modal
|
||||
@click="action = 'uninstall'"
|
||||
>
|
||||
<icon iname="trash-o" /> {{ $t('uninstall') }}
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
<b-button @click="uninstall" id="uninstall" variant="danger">
|
||||
<icon iname="trash-o" /> {{ $t('uninstall') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- EXPERIMENTAL (displayed if experimental feature has been enabled in web-admin options)-->
|
||||
<b-card v-if="this.$store.getters.experimental">
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="flask" /> {{ $t('experimental') }}</h2>
|
||||
</template>
|
||||
|
||||
<card v-if="experimental" :title="$t('experimental')" icon="flask">
|
||||
<!-- APP ACTIONS -->
|
||||
<b-form-group label-cols-md="4" :label="$t('app_actions_label')" label-for="actions">
|
||||
<b-input-group>
|
||||
<b-button id="actions" variant="warning" :to="{ name: 'app-actions', params: { id } }">
|
||||
<icon iname="flask" /> {{ $t('app_actions') }}
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
<b-form-group
|
||||
:label="$t('app_actions_label')" label-for="actions"
|
||||
label-cols-md="4" label-class="font-weight-bold"
|
||||
>
|
||||
<b-button id="actions" variant="warning" :to="{ name: 'app-actions', params: { id } }">
|
||||
<icon iname="flask" /> {{ $t('app_actions') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
|
||||
<!-- APP CONFIG PANEL -->
|
||||
<b-form-group label-cols-md="4" :label="$t('app_config_panel_label')" label-for="config">
|
||||
<b-input-group>
|
||||
<b-button id="config" variant="warning" :to="{ name: 'app-config-panel', params: { id } }">
|
||||
<icon iname="flask" /> {{ $t('app_config_panel') }}
|
||||
</b-button>
|
||||
</b-input-group>
|
||||
<b-form-group
|
||||
:label="$t('app_config_panel_label')" label-for="config"
|
||||
label-cols-md="4" label-class="font-weight-bold"
|
||||
>
|
||||
<b-button id="config" variant="warning" :to="{ name: 'app-config-panel', params: { id } }">
|
||||
<icon iname="flask" /> {{ $t('app_config_panel') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- MODAL -->
|
||||
<b-modal
|
||||
v-if="action"
|
||||
id="modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="actions[action].method" hide-header
|
||||
>
|
||||
{{ $t(actions[action].text, actions[action].name ? { name: actions[action].name } : {}) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<template #skeleton>
|
||||
<card-info-skeleton :item-count="8" />
|
||||
<card-form-skeleton />
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import api from '@/api'
|
||||
|
@ -193,36 +166,27 @@ export default {
|
|||
name: 'AppInfo',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
info: undefined,
|
||||
queries: [
|
||||
`apps/${this.id}?full`,
|
||||
{ uri: 'users/permissions?full', storeKey: 'permissions' },
|
||||
{ uri: 'domains' }
|
||||
],
|
||||
infos: undefined,
|
||||
app: undefined,
|
||||
form: undefined,
|
||||
actions: {
|
||||
changeUrl: { method: this.changeUrl, text: 'confirm_app_change_url' },
|
||||
setAsDefaultDomain: { method: this.setAsDefaultDomain, text: 'confirm_app_default' },
|
||||
uninstall: { method: this.uninstall, text: 'confirm_uninstall', name: this.id }
|
||||
},
|
||||
action: undefined
|
||||
form: undefined
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
readableDate
|
||||
},
|
||||
|
||||
computed: {
|
||||
domains () {
|
||||
return this.$store.state.data.domains
|
||||
},
|
||||
...mapGetters(['domains', 'experimental']),
|
||||
|
||||
allowedGroups () {
|
||||
if (!this.app) return
|
||||
return this.app.permissions[0].allowed
|
||||
}
|
||||
},
|
||||
|
@ -239,67 +203,62 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
Promise.all([
|
||||
api.get(`apps/${this.id}?full`),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'users/permissions?full', storeKey: 'permissions' },
|
||||
{ uri: 'domains' }
|
||||
])
|
||||
]).then(([app]) => {
|
||||
const form = { labels: [] }
|
||||
formatAppData (app) {
|
||||
const form = { labels: [] }
|
||||
|
||||
const mainPermission = app.permissions[this.id + '.main']
|
||||
mainPermission.name = this.id + '.main'
|
||||
mainPermission.title = this.$i18n.t('permission_main')
|
||||
mainPermission.tileAvailable = mainPermission.url !== null && !mainPermission.url.startsWith('re:')
|
||||
form.labels.push({ label: mainPermission.label, show_tile: mainPermission.show_tile })
|
||||
const mainPermission = app.permissions[this.id + '.main']
|
||||
mainPermission.name = this.id + '.main'
|
||||
mainPermission.title = this.$i18n.t('permission_main')
|
||||
mainPermission.tileAvailable = mainPermission.url !== null && !mainPermission.url.startsWith('re:')
|
||||
form.labels.push({ label: mainPermission.label, show_tile: mainPermission.show_tile })
|
||||
|
||||
const permissions = [mainPermission]
|
||||
for (const [name, perm] of Object.entries(app.permissions)) {
|
||||
if (!name.endsWith('.main')) {
|
||||
permissions.push({
|
||||
...perm,
|
||||
name,
|
||||
label: perm.sublabel,
|
||||
title: humanPermissionName(name),
|
||||
tileAvailable: perm.url !== null && !perm.url.startsWith('re:')
|
||||
})
|
||||
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
||||
}
|
||||
const permissions = [mainPermission]
|
||||
for (const [name, perm] of Object.entries(app.permissions)) {
|
||||
if (!name.endsWith('.main')) {
|
||||
permissions.push({
|
||||
...perm,
|
||||
name,
|
||||
label: perm.sublabel,
|
||||
title: humanPermissionName(name),
|
||||
tileAvailable: perm.url !== null && !perm.url.startsWith('re:')
|
||||
})
|
||||
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
||||
}
|
||||
}
|
||||
|
||||
this.info = {
|
||||
id: this.id,
|
||||
label: mainPermission.label,
|
||||
description: app.description,
|
||||
version: app.version,
|
||||
multi_instance: this.$i18n.t(app.manifest.multi_instance ? 'yes' : 'no'),
|
||||
install_time: readableDate(app.settings.install_time, true, true)
|
||||
}
|
||||
if (app.settings.domain) {
|
||||
this.info.url = 'https://' + app.settings.domain + app.settings.path
|
||||
form.url = {
|
||||
domain: app.settings.domain,
|
||||
path: app.settings.path.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
this.form = form
|
||||
this.app = {
|
||||
this.infos = {
|
||||
id: this.id,
|
||||
label: mainPermission.label,
|
||||
description: app.description,
|
||||
version: app.version,
|
||||
multi_instance: this.$i18n.t(app.manifest.multi_instance ? 'yes' : 'no'),
|
||||
install_time: readableDate(app.settings.install_time, true, true)
|
||||
}
|
||||
if (app.settings.domain) {
|
||||
this.infos.url = 'https://' + app.settings.domain + app.settings.path
|
||||
form.url = {
|
||||
domain: app.settings.domain,
|
||||
supports_change_url: app.supports_change_url,
|
||||
permissions
|
||||
path: app.settings.path.slice(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.form = form
|
||||
this.app = {
|
||||
domain: app.settings.domain,
|
||||
supports_change_url: app.supports_change_url,
|
||||
permissions
|
||||
}
|
||||
},
|
||||
|
||||
changeLabel (permName, data) {
|
||||
data.show_tile = data.show_tile ? 'True' : 'False'
|
||||
api.put('users/permissions/' + permName, data).then(this.fetchData)
|
||||
api.put('users/permissions/' + permName, data).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
changeUrl () {
|
||||
async changeUrl () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_change_url'))
|
||||
if (!confirmed) return
|
||||
|
||||
const { domain, path } = this.form.url
|
||||
api.put(
|
||||
`apps/${this.id}/changeurl`,
|
||||
|
@ -307,21 +266,26 @@ export default {
|
|||
).then(this.fetchData)
|
||||
},
|
||||
|
||||
setAsDefaultDomain () {
|
||||
api.put(`apps/${this.id}/default`).then(this.fetchData)
|
||||
async setAsDefaultDomain () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_default'))
|
||||
if (!confirmed) return
|
||||
|
||||
api.put(`apps/${this.id}/default`).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
uninstall () {
|
||||
async uninstall () {
|
||||
const confirmed = await this.$askConfirmation(
|
||||
this.$i18n.t('confirm_uninstall', { name: this.id })
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
api.delete('apps/' + this.id).then(() => {
|
||||
this.$router.push({ name: 'app-list' })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
filters: { readableDate },
|
||||
mixins: [validationMixin]
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<template>
|
||||
<div class="app-install">
|
||||
<div v-if="infos">
|
||||
<view-base :loading="loading">
|
||||
<template v-if="infos">
|
||||
<!-- BASIC INFOS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="info-circle" /> {{ $t('infos') }} — {{ name }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="`${$t('infos')} — ${name}`" icon="info-circle">
|
||||
<b-row
|
||||
v-for="(info, key) in infos" :key="key"
|
||||
no-gutters class="row-line"
|
||||
|
@ -19,16 +15,16 @@
|
|||
<span>{{ info }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- INSTALL FORM -->
|
||||
<card-form
|
||||
:title="$t('operations')" icon="wrench" :submit-text="$t('install')"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="beforeInstall"
|
||||
@submit.prevent="performInstall"
|
||||
>
|
||||
<template v-if="formDisclaimer" #disclaimer>
|
||||
<b-alert show variant="info" v-html="formDisclaimer" />
|
||||
<div class="alert alert-info" v-html="formDisclaimer" />
|
||||
</template>
|
||||
|
||||
<form-field
|
||||
|
@ -36,30 +32,26 @@
|
|||
v-bind="field" v-model="form[fname]" :validation="$v.form[fname]"
|
||||
/>
|
||||
</card-form>
|
||||
|
||||
<!-- CONFIRM INSTALL DOMAIN ROOT MODAL -->
|
||||
<b-modal
|
||||
id="confirm-domain-root-modal" ref="confirm-domain-root-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="performInstall" hide-header
|
||||
:ok-title="$t('install')"
|
||||
>
|
||||
{{ $t('confirm_install_domain_root', { domain: this.form.domain }) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- In case of a custom url with no manifest found -->
|
||||
<b-alert v-else-if="infos === null" variant="warning" show>
|
||||
<b-alert v-else-if="infos === null" variant="warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_install_custom_no_manifest') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
|
||||
<template #skeleton>
|
||||
<card-info-skeleton />
|
||||
<card-form-skeleton :cols="null" />
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { formatYunoHostArguments, formatI18nField, formatFormData } from '@/helpers/yunohostArguments'
|
||||
|
||||
import api from '@/api'
|
||||
import { objectToParams } from '@/helpers/commons'
|
||||
import { formatYunoHostArguments, formatI18nField, formatFormData } from '@/helpers/yunohostArguments'
|
||||
|
||||
export default {
|
||||
name: 'AppInstall',
|
||||
|
@ -67,14 +59,12 @@ export default {
|
|||
mixins: [validationMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
name: undefined,
|
||||
infos: undefined,
|
||||
formDisclaimer: null,
|
||||
|
@ -106,19 +96,7 @@ export default {
|
|||
return api.get('appscatalog?full').then(response => response.apps[this.id].manifest)
|
||||
},
|
||||
|
||||
fetchData () {
|
||||
const isCustom = this.$route.name === 'app-install-custom'
|
||||
Promise.all([
|
||||
isCustom ? this.getExternalManifest() : this.getApiManifest(),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
])
|
||||
]).then((responses) => this.setupForm(responses[0]))
|
||||
},
|
||||
|
||||
setupForm (manifest) {
|
||||
formatManifestData (manifest) {
|
||||
this.name = manifest.name
|
||||
const infosKeys = ['id', 'description', 'license', 'version', 'multi_instance']
|
||||
if (manifest.license === undefined || manifest.license === 'free') {
|
||||
|
@ -137,17 +115,17 @@ export default {
|
|||
this.fields = fields
|
||||
this.form = form
|
||||
this.validations = { form: validations }
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
beforeInstall () {
|
||||
async performInstall () {
|
||||
if ('path' in this.form && this.form.path === '/') {
|
||||
this.$refs['confirm-domain-root-modal'].show()
|
||||
} else {
|
||||
this.performInstall()
|
||||
const confirmed = await this.$askConfirmation(
|
||||
this.$i18n.t('confirm_install_domain_root', { domain: this.form.domain })
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
},
|
||||
|
||||
performInstall () {
|
||||
const { data: args, label } = formatFormData(this.form, { extract: ['label'] })
|
||||
const data = { app: this.id, label, args: objectToParams(args) }
|
||||
|
||||
|
@ -160,7 +138,15 @@ export default {
|
|||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
const isCustom = this.$route.name === 'app-install-custom'
|
||||
Promise.all([
|
||||
isCustom ? this.getExternalManifest() : this.getApiManifest(),
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'users' }
|
||||
])
|
||||
]).then((responses) => this.formatManifestData(responses[0]))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<search-view
|
||||
id="app-list"
|
||||
<view-search
|
||||
:search.sync="search"
|
||||
items-name="installed_apps"
|
||||
:items="apps"
|
||||
:filtered-items="filteredApps"
|
||||
items-name="installed_apps"
|
||||
:queries="queries"
|
||||
@queries-response="formatAppData"
|
||||
>
|
||||
<template #top-bar-buttons>
|
||||
<b-button variant="success" :to="{ name: 'app-catalog' }">
|
||||
|
@ -32,18 +33,16 @@
|
|||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</search-view>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import SearchView from '@/components/SearchView'
|
||||
|
||||
export default {
|
||||
name: 'AppList',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: ['apps?full'],
|
||||
search: '',
|
||||
apps: undefined
|
||||
}
|
||||
|
@ -56,48 +55,40 @@ export default {
|
|||
const match = (item) => item && item.toLowerCase().includes(search)
|
||||
// Check if any value in apps (label, id, name, description) match the search query.
|
||||
const filtered = this.apps.filter(app => Object.values(app).some(match))
|
||||
return filtered.length > 0 ? filtered : null
|
||||
return filtered.length ? filtered : null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get('apps?full').then(({ apps }) => {
|
||||
if (apps.length === 0) {
|
||||
this.apps = null
|
||||
return
|
||||
}
|
||||
formatAppData ({ apps }) {
|
||||
if (apps.length === 0) {
|
||||
this.apps = null
|
||||
return
|
||||
}
|
||||
|
||||
const multiInstances = {}
|
||||
this.apps = apps.map(({ id, name, description, permissions, manifest }) => {
|
||||
// FIXME seems like some apps may no have a label (replace with id)
|
||||
const label = permissions[id + '.main'].label
|
||||
// Display the `id` of the instead of its `name` if multiple apps share the same name
|
||||
if (manifest.multi_instance) {
|
||||
if (!(name in multiInstances)) {
|
||||
multiInstances[name] = []
|
||||
}
|
||||
const labels = multiInstances[name]
|
||||
if (labels.includes(label)) {
|
||||
name = id
|
||||
}
|
||||
labels.push(label)
|
||||
const multiInstances = {}
|
||||
this.apps = apps.map(({ id, name, description, permissions, manifest }) => {
|
||||
// FIXME seems like some apps may no have a label (replace with id)
|
||||
const label = permissions[id + '.main'].label
|
||||
// Display the `id` of the instead of its `name` if multiple apps share the same name
|
||||
if (manifest.multi_instance) {
|
||||
if (!(name in multiInstances)) {
|
||||
multiInstances[name] = []
|
||||
}
|
||||
if (label === name) {
|
||||
name = null
|
||||
const labels = multiInstances[name]
|
||||
if (labels.includes(label)) {
|
||||
name = id
|
||||
}
|
||||
return { id, name, description, label }
|
||||
}).sort((prev, app) => {
|
||||
return prev.label > app.label ? 1 : -1
|
||||
})
|
||||
labels.push(label)
|
||||
}
|
||||
if (label === name) {
|
||||
name = null
|
||||
}
|
||||
return { id, name, description, label }
|
||||
}).sort((prev, app) => {
|
||||
return prev.label > app.label ? 1 : -1
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
components: { SearchView }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="backup">
|
||||
<div>
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="{ id, name, uri } in storages" :key="id"
|
||||
|
@ -7,11 +7,13 @@
|
|||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div>
|
||||
<h5>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ name }}
|
||||
<small>{{ id }}</small>
|
||||
<small class="text-secondary">{{ id }}</small>
|
||||
</h5>
|
||||
<p class="mb-0">{{ uri }}</p>
|
||||
<p class="m-0">
|
||||
{{ uri }}
|
||||
</p>
|
||||
</div>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
|
|
|
@ -1,162 +1,118 @@
|
|||
<template>
|
||||
<div class="backup-create" v-if="isReady">
|
||||
<b-card no-body>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="archive" /> {{ $t('backup_create') }}</h2>
|
||||
</template>
|
||||
|
||||
<view-base :queries="queries" @queries-response="formatData" skeleton="card-list-skeleton">
|
||||
<!-- FIXME switch to <card-form> ? -->
|
||||
<card :title="$t('backup_create')" icon="archive" no-body>
|
||||
<b-form-checkbox-group
|
||||
v-model="selected"
|
||||
id="backup-select" name="backup-select" size="lg"
|
||||
aria-describedby="backup-restore-feedback"
|
||||
>
|
||||
<b-list-group flush>
|
||||
<!-- SYSTEM TITLE -->
|
||||
<b-list-group-item class="d-flex align-items-md-center flex-column flex-md-row" variant="dark">
|
||||
<div>
|
||||
<h4 class="mb-0"><icon iname="cube" /> {{ $t('system') }}</h4>
|
||||
</div>
|
||||
<!-- SYSTEM HEADER -->
|
||||
<b-list-group-item class="d-flex align-items-sm-center flex-column flex-sm-row" variant="light">
|
||||
<h4 class="m-0">
|
||||
<icon iname="cube" /> {{ $t('system') }}
|
||||
</h4>
|
||||
|
||||
<div class="ml-md-auto mt-2 mt-md-0">
|
||||
<div class="ml-sm-auto mt-2 mt-sm-0">
|
||||
<b-button
|
||||
size="sm" variant="light"
|
||||
v-t="'select_all'" @click="toggleSelected(true, 'hooks')"
|
||||
@click="toggleSelected(true, 'system')" v-t="'select_all'"
|
||||
size="sm" variant="outline-dark"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
size="sm" variant="light" class="ml-2"
|
||||
v-t="'select_none'" @click="toggleSelected(false, 'hooks')"
|
||||
@click="toggleSelected(false, 'system')" v-t="'select_none'"
|
||||
size="sm" variant="outline-dark" class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
|
||||
<!-- SYSTEM ITEMS -->
|
||||
<b-list-group-item
|
||||
v-for="(item, partName) in hooks" :key="partName"
|
||||
v-for="(item, partName) in system" :key="partName"
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<h5>{{ item.name }} </h5>
|
||||
<p class="mb-0">{{ item.description }}</p>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ item.name }}
|
||||
</h5>
|
||||
<p class="m-0">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<b-form-checkbox :value="partName" :aria-label="$t('check')" class="d-inline" />
|
||||
</b-list-group-item>
|
||||
|
||||
<!-- APPS TITLE -->
|
||||
<b-list-group-item class="d-flex align-items-md-center flex-column flex-md-row" variant="dark">
|
||||
<div>
|
||||
<h4 class="mb-0"><icon iname="cubes" /> {{ $t('applications') }}</h4>
|
||||
</div>
|
||||
<!-- APPS HEADER -->
|
||||
<b-list-group-item class="d-flex align-items-sm-center flex-column flex-sm-row" variant="light">
|
||||
<h4 class="m-0">
|
||||
<icon iname="cubes" /> {{ $t('applications') }}
|
||||
</h4>
|
||||
|
||||
<div class="ml-md-auto mt-2 mt-md-0">
|
||||
<div class="ml-sm-auto mt-2 mt-sm-0">
|
||||
<b-button
|
||||
size="sm" variant="light"
|
||||
v-t="'select_all'" @click="toggleSelected(true, 'apps')"
|
||||
@click="toggleSelected(true, 'apps')" v-t="'select_all'"
|
||||
size="sm" variant="outline-dark"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
size="sm" variant="light" class="ml-2"
|
||||
v-t="'select_none'" @click="toggleSelected(false, 'apps')"
|
||||
@click="toggleSelected(false, 'apps')" v-t="'select_none'"
|
||||
size="sm" variant="outline-dark" class="ml-2"
|
||||
/>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
|
||||
<!-- APPS ITEMS -->
|
||||
<b-list-group-item
|
||||
v-for="(item, appName) in apps" :key="appName"
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<h5>{{ item.name }} <small>{{ item.id }}</small></h5>
|
||||
<p class="mb-0">{{ item.description }}</p>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ item.name }} <small class="text-secondary">{{ item.id }}</small>
|
||||
</h5>
|
||||
<p class="m-0">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<b-form-checkbox :value="appName" :aria-label="$t('check')" class="d-inline"/>
|
||||
<b-form-checkbox :value="appName" :aria-label="$t('check')" class="d-inline" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-form-checkbox-group>
|
||||
|
||||
<!-- SUBMIT -->
|
||||
<template v-slot:footer>
|
||||
<div class="d-flex justify-content-end">
|
||||
<b-button
|
||||
@click="createBackup" variant="success"
|
||||
v-t="'backup_action'" :disabled="selected.length === 0"
|
||||
/>
|
||||
</div>
|
||||
<template #buttons>
|
||||
<b-button
|
||||
@click="createBackup" v-t="'backup_action'"
|
||||
variant="success" :disabled="selected.length === 0"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</div>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'BackupCreate',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isReady: false,
|
||||
queries: ['hooks/backup', 'apps?with_backup'],
|
||||
selected: [],
|
||||
// api data
|
||||
hooks: undefined,
|
||||
system: undefined,
|
||||
apps: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.getAll(['hooks/backup', 'apps?with_backup']).then(([{ hooks }, { apps }]) => {
|
||||
this.hooks = this.formatHooks(hooks)
|
||||
// transform app array into literal object to match hooks data structure
|
||||
this.apps = apps.reduce((obj, app) => {
|
||||
obj[app.id] = app
|
||||
return obj
|
||||
}, {})
|
||||
this.selected = [...Object.keys(this.hooks), ...Object.keys(this.apps)]
|
||||
this.isReady = true
|
||||
})
|
||||
},
|
||||
|
||||
toggleSelected (select, type) {
|
||||
if (select) {
|
||||
const toSelect = Object.keys(this[type]).filter(item => !this.selected.includes(item))
|
||||
this.selected = [...this.selected, ...toSelect]
|
||||
} else {
|
||||
const toUnselect = Object.keys(this[type])
|
||||
this.selected = this.selected.filter(selected => !toUnselect.includes(selected))
|
||||
}
|
||||
},
|
||||
|
||||
createBackup () {
|
||||
const data = {
|
||||
apps: [],
|
||||
system: []
|
||||
}
|
||||
|
||||
for (const item of this.selected) {
|
||||
if (item in this.hooks) {
|
||||
data.system = [...data.system, ...this.hooks[item].value]
|
||||
} else {
|
||||
data.apps.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
api.post('backup', data).then(response => {
|
||||
// FIXME display ws messages
|
||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||
})
|
||||
},
|
||||
|
||||
formatHooks (hooks) {
|
||||
const data = {}
|
||||
hooks.forEach(hook => {
|
||||
|
@ -173,11 +129,42 @@ export default {
|
|||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
formatData ({ hooks }, { apps }) {
|
||||
this.system = this.formatHooks(hooks)
|
||||
// transform app array into literal object to match hooks data structure
|
||||
this.apps = apps.reduce((obj, app) => {
|
||||
obj[app.id] = app
|
||||
return obj
|
||||
}, {})
|
||||
this.selected = [...Object.keys(this.system), ...Object.keys(this.apps)]
|
||||
},
|
||||
|
||||
toggleSelected (select, type) {
|
||||
if (select) {
|
||||
const toSelect = Object.keys(this[type]).filter(item => !this.selected.includes(item))
|
||||
this.selected = [...this.selected, ...toSelect]
|
||||
} else {
|
||||
const toUnselect = Object.keys(this[type])
|
||||
this.selected = this.selected.filter(selected => !toUnselect.includes(selected))
|
||||
}
|
||||
},
|
||||
|
||||
createBackup () {
|
||||
const data = { apps: [], system: [] }
|
||||
for (const item of this.selected) {
|
||||
if (item in this.system) {
|
||||
data.system = [...data.system, ...this.system[item].value]
|
||||
} else {
|
||||
data.apps.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
api.post('backup', data).then(response => {
|
||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,82 +1,68 @@
|
|||
<template>
|
||||
<div class="backup-info" v-if="isReady">
|
||||
<view-base :queries="queries" @queries-response="formatBackupData">
|
||||
<!-- BACKUP INFO -->
|
||||
<b-card no-body>
|
||||
<b-card-header class="d-flex align-items-md-center flex-column flex-md-row">
|
||||
<div>
|
||||
<h2><icon iname="info-circle" /> {{ $t('infos') }}</h2>
|
||||
</div>
|
||||
<card :title="$t('infos')" icon="info-circle" button-unbreak="sm">
|
||||
<template #header-buttons>
|
||||
<!-- DOWNLOAD ARCHIVE -->
|
||||
<b-button @click="downloadBackup" size="sm" variant="success">
|
||||
<icon iname="download" /> {{ $t('download') }}
|
||||
</b-button>
|
||||
|
||||
<div class="ml-md-auto mt-2 mt-md-0">
|
||||
<!-- DOWNLOAD ARCHIVE -->
|
||||
<b-button size="sm" variant="success" @click="downloadBackup">
|
||||
<icon iname="download" /> {{ $t('download') }}
|
||||
</b-button>
|
||||
<!-- DELETE ARCHIVE -->
|
||||
<b-button @click="deleteBackup" size="sm" variant="danger">
|
||||
<icon iname="trash-o" /> {{ $t('delete') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<!-- DELETE ARCHIVE -->
|
||||
<b-button
|
||||
size="sm" variant="danger" id="delete-backup"
|
||||
class="ml-2" v-b-modal.confirm-delete-backup
|
||||
>
|
||||
<icon iname="trash-o" /> {{ $t('delete') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body>
|
||||
<b-row
|
||||
v-for="(value, prop) in info" :key="prop"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col cols="5" md="3" xl="3">
|
||||
<strong>{{ $t(prop === 'name' ? 'id' : prop) }}</strong>
|
||||
<span class="sep" />
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span v-if="prop === 'created_at'">{{ value | readableDate }}</span>
|
||||
<span v-else-if="prop === 'size'">{{ value | humanSize }}</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
<b-row
|
||||
v-for="(value, prop) in infos" :key="prop"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col md="3" xl="2">
|
||||
<strong>{{ $t(prop === 'name' ? 'id' : prop) }}</strong>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span v-if="prop === 'created_at'">{{ value | readableDate }}</span>
|
||||
<span v-else-if="prop === 'size'">{{ value | humanSize }}</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</card>
|
||||
|
||||
<!-- BACKUP CONTENT -->
|
||||
<b-card no-body>
|
||||
<b-card-header class="d-flex align-items-md-center flex-column flex-md-row">
|
||||
<div>
|
||||
<h2><icon iname="archive" /> {{ $t('backup_content') }}</h2>
|
||||
</div>
|
||||
<!-- FIXME switch to <card-form> ? -->
|
||||
<card
|
||||
:title="$t('backup_content')" icon="archive"
|
||||
no-body button-unbreak="sm"
|
||||
>
|
||||
<template #header-buttons>
|
||||
<b-button
|
||||
size="sm" variant="outline-secondary"
|
||||
@click="toggleSelected()" v-t="'select_all'"
|
||||
/>
|
||||
|
||||
<div class="ml-md-auto mt-2 mt-md-0">
|
||||
<b-button
|
||||
size="sm" variant="outline-secondary"
|
||||
v-t="'select_all'"
|
||||
@click="toggleSelected()"
|
||||
/>
|
||||
|
||||
<b-button
|
||||
size="sm" variant="outline-secondary" class="ml-2"
|
||||
v-t="'select_none'"
|
||||
@click="toggleSelected(false)"
|
||||
/>
|
||||
</div>
|
||||
</b-card-header>
|
||||
<b-button
|
||||
size="sm" variant="outline-secondary"
|
||||
@click="toggleSelected(false)" v-t="'select_none'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<b-form-checkbox-group
|
||||
v-if="hasItems" v-model="selected"
|
||||
v-if="hasBackupData" v-model="selected"
|
||||
id="backup-select" name="backup-select" size="lg"
|
||||
aria-describedby="backup-restore-feedback"
|
||||
>
|
||||
<b-list-group flush>
|
||||
<!-- SYSTEM PARTS -->
|
||||
<b-list-group-item
|
||||
v-for="(item, partName) in systemParts" :key="partName"
|
||||
v-for="(item, partName) in system" :key="partName"
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<h5>{{ item.name }} <small v-if="item.size">({{ item.size | humanSize }})</small></h5>
|
||||
<p class="mb-0">
|
||||
<h5 class="font-weight-bold">
|
||||
{{ item.name }} <small class="text-secondary" v-if="item.size">({{ item.size | humanSize }})</small>
|
||||
</h5>
|
||||
<p class="m-0">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -90,8 +76,10 @@
|
|||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div class="mr-2">
|
||||
<h5>{{ item.name }} <small>{{ appName }} ({{ item.size | humanSize }})</small></h5>
|
||||
<p class="mb-0">
|
||||
<h5 class="font-weight-bold">
|
||||
{{ item.name }} <small class="text-secondary">{{ appName }} ({{ item.size | humanSize }})</small>
|
||||
</h5>
|
||||
<p class="m-0">
|
||||
{{ $t('version') }} {{ item.version }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -101,155 +89,66 @@
|
|||
</b-list-group>
|
||||
|
||||
<b-form-invalid-feedback id="backup-restore-feedback" :state="isValid">
|
||||
<b-alert variant="danger" show class="mb-0">
|
||||
<b-alert variant="danger" class="mb-0">
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-checkbox-group>
|
||||
|
||||
<b-alert
|
||||
v-else
|
||||
variant="warning" class="mb-0" show
|
||||
>
|
||||
<div v-else class="alert alert-warning mb-0">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('archive_empty') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
|
||||
<!-- SUBMIT -->
|
||||
<template v-if="hasItems" v-slot:footer>
|
||||
<div class="d-flex justify-content-end">
|
||||
<b-button
|
||||
v-b-modal.confirm-restore-backup form="backup-restore" variant="success"
|
||||
v-t="'restore'" :disabled="selected.length === 0"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="hasBackupData" #buttons>
|
||||
<b-button
|
||||
@click="restoreBackup" form="backup-restore" variant="success"
|
||||
v-t="'restore'" :disabled="selected.length === 0"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- RESTORE BACKUP MODAL -->
|
||||
<b-modal
|
||||
id="confirm-restore-backup" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="restoreBackup" hide-header
|
||||
>
|
||||
{{ $t('confirm_restore', { name }) }}
|
||||
</b-modal>
|
||||
|
||||
<!-- DELETE BACKUP MODAL -->
|
||||
<b-modal
|
||||
id="confirm-delete-backup" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="deleteBackup" hide-header
|
||||
>
|
||||
{{ $t('confirm_delete', { name }) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<template #skeleton>
|
||||
<card-info-skeleton :item-count="4" />
|
||||
<card-list-skeleton />
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { readableDate } from '@/helpers/filters/date'
|
||||
import { humanSize } from '@/helpers/filters/human'
|
||||
import { isEmptyValue } from '@/helpers/commons'
|
||||
|
||||
export default {
|
||||
name: 'BackupInfo',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true },
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isReady: false,
|
||||
restore: false,
|
||||
queries: [`backup/archives/${this.name}?with_details`],
|
||||
selected: [],
|
||||
error: '',
|
||||
isValid: null,
|
||||
// api data
|
||||
info: {
|
||||
name: this.name,
|
||||
created_at: undefined,
|
||||
size: undefined,
|
||||
path: undefined
|
||||
},
|
||||
infos: undefined,
|
||||
apps: undefined,
|
||||
systemParts: undefined
|
||||
system: undefined
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
readableDate,
|
||||
humanSize
|
||||
computed: {
|
||||
hasBackupData () {
|
||||
return !isEmptyValue(this.system) || !isEmptyValue(this.apps)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get(`backup/archives/${this.name}?with_details`).then((data) => {
|
||||
this.info.created_at = data.created_at
|
||||
this.info.size = data.size
|
||||
this.info.path = data.path
|
||||
this.hasItems = Object.keys(data.system).length !== 0 || Object.keys(data.apps).length !== 0
|
||||
this.systemParts = this.formatHooks(data.system)
|
||||
this.apps = data.apps
|
||||
|
||||
this.toggleSelected()
|
||||
this.isReady = true
|
||||
})
|
||||
},
|
||||
|
||||
toggleSelected (select = true) {
|
||||
if (select) {
|
||||
this.selected = [
|
||||
...Object.keys(this.apps),
|
||||
...Object.keys(this.systemParts)
|
||||
]
|
||||
} else {
|
||||
this.selected = []
|
||||
}
|
||||
},
|
||||
|
||||
restoreBackup () {
|
||||
const data = {
|
||||
apps: [],
|
||||
system: [],
|
||||
force: ''
|
||||
}
|
||||
|
||||
for (const item of this.selected) {
|
||||
if (item in this.systemParts) {
|
||||
data.system = [...data.system, ...this.systemParts[item].value]
|
||||
} else {
|
||||
data.apps.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
api.post('backup/restore/' + this.name, data).then(response => {
|
||||
// FIXME display ws messages
|
||||
this.isValid = null
|
||||
}).catch(err => {
|
||||
// FIXME some errors may be sent by the websocket (yunohost api error for exemple)
|
||||
this.error = err.message
|
||||
this.isValid = false
|
||||
})
|
||||
},
|
||||
|
||||
deleteBackup () {
|
||||
api.delete('backup/archives/' + this.name).then(() => {
|
||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||
})
|
||||
},
|
||||
|
||||
downloadBackup () {
|
||||
const host = this.$store.getters.host
|
||||
window.open(`https://${host}/yunohost/api/backup/download/${this.name}`, '_blank')
|
||||
},
|
||||
|
||||
formatHooks (hooks) {
|
||||
const data = {}
|
||||
Object.entries(hooks).forEach(([hook, { size }]) => {
|
||||
|
@ -268,11 +167,73 @@ export default {
|
|||
}
|
||||
})
|
||||
return data
|
||||
},
|
||||
|
||||
formatBackupData (data) {
|
||||
this.infos = {
|
||||
name: this.name,
|
||||
created_at: data.created_at,
|
||||
size: data.size,
|
||||
path: data.path
|
||||
}
|
||||
this.system = this.formatHooks(data.system)
|
||||
this.apps = data.apps
|
||||
|
||||
this.toggleSelected()
|
||||
},
|
||||
|
||||
toggleSelected (select = true) {
|
||||
if (select) {
|
||||
this.selected = [
|
||||
...Object.keys(this.apps),
|
||||
...Object.keys(this.system)
|
||||
]
|
||||
} else {
|
||||
this.selected = []
|
||||
}
|
||||
},
|
||||
|
||||
async restoreBackup () {
|
||||
const confirmed = await this.$askConfirmation(
|
||||
this.$i18n.t('confirm_restore', { name: this.name })
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
const data = { apps: [], system: [], force: '' }
|
||||
for (const item of this.selected) {
|
||||
if (item in this.system) {
|
||||
data.system = [...data.system, ...this.system[item].value]
|
||||
} else {
|
||||
data.apps.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
api.post('backup/restore/' + this.name, data).then(response => {
|
||||
this.isValid = null
|
||||
}).catch(err => {
|
||||
this.error = err.message
|
||||
this.isValid = false
|
||||
})
|
||||
},
|
||||
|
||||
async deleteBackup () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: this.name }))
|
||||
if (!confirmed) return
|
||||
|
||||
api.delete('backup/archives/' + this.name).then(() => {
|
||||
this.$router.push({ name: 'backup-list', params: { id: this.id } })
|
||||
})
|
||||
},
|
||||
|
||||
downloadBackup () {
|
||||
const host = this.$store.getters.host
|
||||
window.open(`https://${host}/yunohost/api/backup/download/${this.name}`, '_blank')
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
filters: {
|
||||
readableDate,
|
||||
humanSize
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="backup-list">
|
||||
<view-top-bar :button="{ text: $t('backup_new'), icon: 'plus', to: { name: 'backup-create' } }" />
|
||||
<view-base :queries="queries" @queries-response="formatBackupList" skeleton="list-group-skeleton">
|
||||
<template #top>
|
||||
<top-bar :button="{ text: $t('backup_new'), icon: 'plus', to: { name: 'backup-create' } }" />
|
||||
</template>
|
||||
|
||||
<b-alert v-if="!archives" variant="warning" show>
|
||||
<b-alert v-if="!archives" variant="warning">
|
||||
<icon iname="exclamation-triangle" />
|
||||
{{ $t('items_verbose_count', { items: $tc('items.backups', 0) }) }}
|
||||
</b-alert>
|
||||
|
@ -15,9 +17,9 @@
|
|||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div>
|
||||
<h5>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ created_at | distanceToNow }}
|
||||
<small>{{ name }} ({{ size | humanSize }})</small>
|
||||
<small class="text-secondary">{{ name }} ({{ size | humanSize }})</small>
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
{{ path }}
|
||||
|
@ -26,11 +28,10 @@
|
|||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { distanceToNow, readableDate } from '@/helpers/filters/date'
|
||||
import { humanSize } from '@/helpers/filters/human'
|
||||
|
||||
|
@ -38,35 +39,26 @@ export default {
|
|||
name: 'BackupList',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: ['backup/archives?with_info'],
|
||||
archives: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get('backup/archives?with_info').then(data => {
|
||||
// FIXME use archives = null if no archives
|
||||
const archives = Object.entries(data.archives)
|
||||
this.archives = archives.length === 0 ? null : archives.map(([name, infos]) => {
|
||||
infos.name = name
|
||||
return infos
|
||||
}).reverse()
|
||||
})
|
||||
formatBackupList (data) {
|
||||
const archives = Object.entries(data.archives)
|
||||
this.archives = archives.length === 0 ? null : archives.map(([name, infos]) => {
|
||||
infos.name = name
|
||||
return infos
|
||||
}).reverse()
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
filters: {
|
||||
distanceToNow,
|
||||
readableDate,
|
||||
|
|
|
@ -1,109 +1,107 @@
|
|||
<template>
|
||||
<div class="diagnosis">
|
||||
<view-top-bar>
|
||||
<template #group-right>
|
||||
<b-button @click="shareLogs" variant="success">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</view-top-bar>
|
||||
|
||||
<b-alert variant="info" show>
|
||||
{{ $t(reports ? 'diagnosis_explanation' : 'diagnosis_first_run') }}
|
||||
<b-button
|
||||
v-if="reports === null" @click="runFullDiagnosis"
|
||||
class="d-block mt-2" variant="info"
|
||||
>
|
||||
<icon iname="stethoscope" /> {{ $t('run_first_diagnosis') }}
|
||||
<view-base
|
||||
:loading="loading" ref="view"
|
||||
:queries="queries" @queries-response="formatData"
|
||||
>
|
||||
<template #top-bar-group-right>
|
||||
<b-button @click="shareLogs" variant="success">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</b-alert>
|
||||
</template>
|
||||
|
||||
<b-alert
|
||||
class="mb-5" variant="warning" show
|
||||
v-t="'diagnosis_experimental_disclaimer'"
|
||||
/>
|
||||
<template #top>
|
||||
<div class="alert alert-info">
|
||||
{{ $t(reports || loading ? 'diagnosis_explanation' : 'diagnosis_first_run') }}
|
||||
<b-button
|
||||
v-if="reports === null" class="d-block mt-2" variant="info"
|
||||
@click="runDiagnosis"
|
||||
>
|
||||
<icon iname="stethoscope" /> {{ $t('run_first_diagnosis') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<div v-t="'diagnosis_experimental_disclaimer'" class="alert alert-warning mb-5" />
|
||||
</template>
|
||||
|
||||
<!-- REPORT CARD -->
|
||||
<b-card no-body v-for="({ id, description, noIssues, errors, warnings, ignoreds, timestamp, items }, r) in reports" :key="id">
|
||||
<card
|
||||
v-for="report in reports" :key="report.id"
|
||||
collapsable :collapsed="report.noIssues" button-unbreak="lg"
|
||||
>
|
||||
<!-- REPORT HEADER -->
|
||||
<b-card-header class="d-flex align-items-md-center flex-column flex-md-row">
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>{{ description }}</h2>
|
||||
<template #header>
|
||||
<h2>{{ report.description }}</h2>
|
||||
|
||||
<b-badge
|
||||
v-if="noIssues" pill variant="success"
|
||||
v-t="'everything_good'"
|
||||
/>
|
||||
<b-badge
|
||||
v-if="errors" variant="danger" pill
|
||||
v-t="{ path: 'issues', args: { count: errors } }"
|
||||
/>
|
||||
<b-badge v-if="warnings" variant="warning" v-t="{ path: 'warnings', args: { count: warnings } }" />
|
||||
<b-badge v-if="ignoreds" v-t="{ path: 'ignored', args: { count: ignoreds } }" />
|
||||
<div class="">
|
||||
<b-badge v-if="report.noIssues" variant="success" v-t="'everything_good'" />
|
||||
<b-badge v-if="report.errors" variant="danger" v-t="{ path: 'issues', args: { count: report.errors } }" />
|
||||
<b-badge v-if="report.warnings" variant="warning" v-t="{ path: 'warnings', args: { count: report.warnings } }" />
|
||||
<b-badge v-if="report.ignoreds" v-t="{ path: 'ignored', args: { count: report.ignoreds } }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="d-flex ml-md-auto mt-2 mt-md-0">
|
||||
<b-button size="sm" :variant="items ? 'info' : 'success'" @click="reRunDiagnosis(id)">
|
||||
<icon iname="refresh" /> {{ $t('rerun_diagnosis') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
size="sm" variant="outline-secondary" class="ml-auto ml-md-2"
|
||||
v-b-toggle="'collapse-' + id"
|
||||
>
|
||||
<icon iname="chevron-right" /><span class="sr-only">{{ $t('words.collapse') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
<template #header-buttons>
|
||||
<b-button size="sm" :variant="report.items ? 'info' : 'success'" @click="runDiagnosis(report.id)">
|
||||
<icon iname="refresh" /> {{ $t('rerun_diagnosis') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<!-- REPORT BODY -->
|
||||
<b-collapse :id="'collapse-' + id" :visible="!noIssues">
|
||||
<p class="last-time-run">
|
||||
{{ $t('last_ran') }} {{ timestamp | distanceToNow(true, true) }}
|
||||
</p>
|
||||
<p class="last-time-run">
|
||||
{{ $t('last_ran') }} {{ report.timestamp | distanceToNow(true, true) }}
|
||||
</p>
|
||||
|
||||
<b-list-group flush>
|
||||
<!-- REPORT ITEM -->
|
||||
<b-list-group-item
|
||||
v-for="({ status, icon, summary, ignored, issue, details, filterArgs, meta }, i) in items"
|
||||
:key="i" :variant="status"
|
||||
>
|
||||
<div class="item-button d-flex align-items-center">
|
||||
<icon :iname="icon" class="mr-1" /> <p class="mb-0 mr-2" v-html="summary" />
|
||||
<b-list-group flush>
|
||||
<!-- REPORT ITEM -->
|
||||
<b-list-group-item
|
||||
v-for="(item, i) in report.items" :key="i"
|
||||
:variant="item.variant"
|
||||
>
|
||||
<div class="item-button d-flex align-items-center">
|
||||
<icon :iname="item.icon" class="mr-1" /> <p class="mb-0 mr-2" v-html="item.summary" />
|
||||
|
||||
<div class="d-flex flex-column flex-lg-row ml-auto">
|
||||
<b-button
|
||||
v-if="ignored" size="sm"
|
||||
@click="toggleIgnoreIssue(false, filterArgs, r, i)"
|
||||
>
|
||||
<icon iname="bell" /> <span v-t="'unignore'" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="issue"
|
||||
variant="warning" size="sm" @click="toggleIgnoreIssue(true, filterArgs, r, i)"
|
||||
>
|
||||
<icon iname="bell-slash" /> <span v-t="'ignore'" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="details"
|
||||
size="sm" variant="outline-dark" class="ml-lg-2 mt-2 mt-lg-0"
|
||||
v-b-toggle="'collapse-' + id + '-item-' + i"
|
||||
>
|
||||
<icon iname="level-down" /> <span v-t="'details'" />
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-lg-row ml-auto">
|
||||
<b-button
|
||||
v-if="item.ignored" size="sm"
|
||||
@click="toggleIgnoreIssue(false, report, item)"
|
||||
>
|
||||
<icon iname="bell" /> {{ $t('unignore') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="item.issue" variant="warning" size="sm"
|
||||
@click="toggleIgnoreIssue(true, report, item)"
|
||||
>
|
||||
<icon iname="bell-slash" /> {{ $t('ignore') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="item.details"
|
||||
size="sm" variant="outline-dark" class="ml-lg-2 mt-2 mt-lg-0"
|
||||
v-b-toggle="`collapse-${report.id}-item-${i}`"
|
||||
>
|
||||
<icon iname="level-down" /> {{ $t('details') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-collapse v-if="details" :id="'collapse-' + id + '-item-' + i">
|
||||
<ul class="mt-2 pl-4">
|
||||
<li v-for="(detail, index) in details" :key="index" v-html="detail" />
|
||||
</ul>
|
||||
</b-collapse>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</div>
|
||||
<b-collapse v-if="item.details" :id="`collapse-${report.id}-item-${i}`">
|
||||
<ul class="mt-2 pl-4">
|
||||
<li v-for="(detail, index) in item.details" :key="index" v-html="detail" />
|
||||
</ul>
|
||||
</b-collapse>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<template #skeleton>
|
||||
<card-list-skeleton />
|
||||
<b-card no-body>
|
||||
<template #header>
|
||||
<b-skeleton width="30%" height="36px" class="m-0" />
|
||||
</template>
|
||||
</b-card>
|
||||
<card-list-skeleton />
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -115,72 +113,85 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
queries: ['diagnosis/show?full'],
|
||||
loading: true,
|
||||
reports: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get('diagnosis/show?full').then((data) => {
|
||||
if (data === null) {
|
||||
this.reports = null
|
||||
return
|
||||
formatReportItem (report, item) {
|
||||
let issue = false
|
||||
let icon = ''
|
||||
const status = item.variant = item.status.toLowerCase()
|
||||
|
||||
if (status === 'success') {
|
||||
icon = 'check-circle'
|
||||
} else if (status === 'info') {
|
||||
icon = 'info-circle'
|
||||
} else if (item.ignored) {
|
||||
icon = status !== 'error' ? status : 'times'
|
||||
item.variant = 'light'
|
||||
report.ignoreds++
|
||||
} else if (status === 'warning') {
|
||||
icon = status
|
||||
issue = true
|
||||
report.warnings++
|
||||
} else if (status === 'error') {
|
||||
item.variant = 'danger'
|
||||
icon = 'times'
|
||||
issue = true
|
||||
report.errors++
|
||||
}
|
||||
|
||||
item.issue = issue
|
||||
item.icon = icon
|
||||
},
|
||||
|
||||
formatData (data) {
|
||||
if (data === null) {
|
||||
this.reports = null
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
const reports = data.reports
|
||||
for (const report of reports) {
|
||||
report.warnings = 0
|
||||
report.errors = 0
|
||||
report.ignoreds = 0
|
||||
|
||||
for (var item of report.items) {
|
||||
this.formatReportItem(report, item)
|
||||
}
|
||||
|
||||
const reports = data.reports
|
||||
for (const report of reports) {
|
||||
report.warnings = 0
|
||||
report.errors = 0
|
||||
report.ignoreds = 0
|
||||
|
||||
for (var item of report.items) {
|
||||
let issue = false
|
||||
let icon = ''
|
||||
const status = item.status = item.status.toLowerCase()
|
||||
|
||||
if (status === 'success') {
|
||||
icon = 'check-circle'
|
||||
} else if (status === 'info') {
|
||||
icon = 'info-circle'
|
||||
} else if (item.ignored) {
|
||||
icon = status !== 'error' ? status : 'times'
|
||||
item.status = 'ignored'
|
||||
report.ignoreds++
|
||||
} else if (status === 'warning') {
|
||||
icon = status
|
||||
issue = true
|
||||
report.warnings++
|
||||
} else if (status === 'error') {
|
||||
item.status = 'danger'
|
||||
icon = 'times'
|
||||
issue = true
|
||||
report.errors++
|
||||
}
|
||||
|
||||
item.issue = issue
|
||||
item.icon = icon
|
||||
item.filterArgs = Object.entries(item.meta).reduce((filterArgs, entries) => {
|
||||
filterArgs.push(entries.join('='))
|
||||
return filterArgs
|
||||
}, [report.id])
|
||||
}
|
||||
report.noIssues = report.warnings + report.errors === 0
|
||||
}
|
||||
this.reports = reports
|
||||
})
|
||||
report.noIssues = report.warnings + report.errors === 0
|
||||
}
|
||||
this.reports = reports
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
runFullDiagnosis () {
|
||||
api.post('diagnosis/run').then(this.fetchData)
|
||||
runDiagnosis (id = null) {
|
||||
const param = id !== null ? '?force' : ''
|
||||
const data = id !== null ? { categories: [id] } : {}
|
||||
api.post('diagnosis/run' + param, data).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
reRunDiagnosis (id) {
|
||||
api.post('diagnosis/run?force', { categories: [id] }).then(this.fetchData)
|
||||
},
|
||||
|
||||
toggleIgnoreIssue (ignore, filterArgs, reportIndex, itemIndex) {
|
||||
toggleIgnoreIssue (ignore, report, item) {
|
||||
const key = (ignore ? 'add' : 'remove') + '_filter'
|
||||
api.post('diagnosis/ignore', { [key]: filterArgs }).then(this.fetchData)
|
||||
const filterArgs = Object.entries(item.meta).reduce((filterArgs, entries) => {
|
||||
filterArgs.push(entries.join('='))
|
||||
return filterArgs
|
||||
}, [report.id])
|
||||
|
||||
api.post('diagnosis/ignore', { [key]: filterArgs }).then(() => {
|
||||
item.ignored = ignore
|
||||
if (ignore) {
|
||||
report[item.status.toLowerCase() + 's']--
|
||||
} else {
|
||||
report.ignoreds--
|
||||
}
|
||||
this.formatReportItem(report, item)
|
||||
})
|
||||
},
|
||||
|
||||
shareLogs () {
|
||||
|
@ -191,17 +202,15 @@ export default {
|
|||
},
|
||||
|
||||
created () {
|
||||
api.post('diagnosis/run?except_if_never_ran_yet').then(this.fetchData)
|
||||
api.post('diagnosis/run?except_if_never_ran_yet')
|
||||
},
|
||||
|
||||
filters: {
|
||||
distanceToNow
|
||||
}
|
||||
filters: { distanceToNow }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.badge {
|
||||
.badge + .badge {
|
||||
margin-left: .5rem
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template lang="html">
|
||||
<domain-form
|
||||
:title="$t('domain_add')" :server-error="serverError"
|
||||
@submit="onSubmit" :submit-text="$t('add')"
|
||||
/>
|
||||
<template>
|
||||
<view-base :queries="queries" skeleton="card-form-skeleton">
|
||||
<domain-form
|
||||
:title="$t('domain_add')" :server-error="serverError"
|
||||
@submit="onSubmit" :submit-text="$t('add')"
|
||||
/>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -13,6 +15,7 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
queries: [{ uri: 'domains' }],
|
||||
serverError: ''
|
||||
}
|
||||
},
|
||||
|
@ -33,8 +36,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
DomainForm
|
||||
}
|
||||
components: { DomainForm }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,37 +1,33 @@
|
|||
<template>
|
||||
<div class="domain-cert" v-if="cert">
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="lock" /> {{ $t('certificate_status') }}</h2>
|
||||
</template>
|
||||
|
||||
<view-base :queries="queries" @queries-response="formatCertData" ref="view">
|
||||
<card v-if="cert" :title="$t('certificate_status')" icon="lock">
|
||||
<p :class="'alert alert-' + cert.alert.type">
|
||||
<icon :iname="cert.alert.icon" /> {{ $t('certificate_alert_' + cert.alert.trad) }}
|
||||
</p>
|
||||
|
||||
<dl>
|
||||
<dt v-t="'certificate_authority'" />
|
||||
<dd>{{ cert.type }} ({{ name }})</dd>
|
||||
<hr>
|
||||
<dt v-t="'validity'" />
|
||||
<dd>{{ $tc('day_validity', cert.validity) }}</dd>
|
||||
</dl>
|
||||
</b-card>
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col md="4" xl="2">
|
||||
<strong v-t="'certificate_authority'" />
|
||||
</b-col>
|
||||
<b-col>{{ cert.type }} ({{ name }})</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="wrench" /> {{ $t('operations') }}</h2>
|
||||
</template>
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col md="4" xl="2">
|
||||
<strong v-t="'validity'" />
|
||||
</b-col>
|
||||
<b-col>{{ $tc('day_validity', cert.validity) }}</b-col>
|
||||
</b-row>
|
||||
</card>
|
||||
|
||||
<card v-if="cert" :title="$t('operations')" icon="wrench">
|
||||
<!-- CERT INSTALL LETSENCRYPT -->
|
||||
<template v-if="actionsEnabled.installLetsencrypt">
|
||||
<p>
|
||||
<icon :iname="cert.acmeEligible ? 'check' : 'meh-o'" /> <span v-html="$t(`domain_${cert.acmeEligible ? 'is' : 'not'}_eligible_for_ACME`)" />
|
||||
</p>
|
||||
<b-button
|
||||
variant="success" :disabled="!cert.acmeEligible"
|
||||
@click="action = 'install_LE'" v-b-modal.action-confirm-modal
|
||||
>
|
||||
|
||||
<b-button @click="callAction('install_LE')" variant="success" :disabled="!cert.acmeEligible">
|
||||
<icon iname="star" /> {{ $t('install_letsencrypt_cert') }}
|
||||
</b-button>
|
||||
<hr>
|
||||
|
@ -40,7 +36,8 @@
|
|||
<!-- CERT RENEW LETS-ENCRYPT -->
|
||||
<template v-if="actionsEnabled.manualRenewLetsencrypt">
|
||||
<p v-t="'manually_renew_letsencrypt_message'" />
|
||||
<b-button variant="warning" @click="action = 'manual_renew_LE'" v-b-modal.action-confirm-modal>
|
||||
|
||||
<b-button @click="callAction('manual_renew_LE')" variant="warning">
|
||||
<icon iname="refresh" /> {{ $t('manually_renew_letsencrypt') }}
|
||||
</b-button>
|
||||
<hr>
|
||||
|
@ -49,7 +46,8 @@
|
|||
<!-- CERT REGEN SELF-SIGNED -->
|
||||
<template v-if="actionsEnabled.regenSelfsigned">
|
||||
<p v-t="'regenerate_selfsigned_cert_message'" />
|
||||
<b-button variant="warning" @click="action = 'regen_selfsigned'" v-b-modal.action-confirm-modal>
|
||||
|
||||
<b-button @click="callAction('regen_selfsigned')" variant="warning">
|
||||
<icon iname="refresh" /> {{ $t('regenerate_selfsigned_cert') }}
|
||||
</b-button>
|
||||
<hr>
|
||||
|
@ -58,23 +56,19 @@
|
|||
<!-- CERT REPLACE WITH SELF-SIGNED -->
|
||||
<template v-if="actionsEnabled.replaceWithSelfsigned">
|
||||
<p v-t="'revert_to_selfsigned_cert_message'" />
|
||||
<b-button variant="danger" @click="action = 'revert_to_selfsigned'" v-b-modal.action-confirm-modal>
|
||||
|
||||
<b-button @click="callAction('revert_to_selfsigned')" variant="danger">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('revert_to_selfsigned_cert') }}
|
||||
</b-button>
|
||||
<hr>
|
||||
</template>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- ACTIONS CONFIRMATION MODAL -->
|
||||
<b-modal
|
||||
v-if="action"
|
||||
id="action-confirm-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="callAction" hide-header
|
||||
>
|
||||
{{ $t(`confirm_cert_${action}`) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<template #skeleton>
|
||||
<card-info-skeleton :item-count="2" />
|
||||
<card-buttons-skeleton :item-count="2" />
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -84,96 +78,81 @@ export default {
|
|||
name: 'DomainCert',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [`domains/cert-status/${this.name}?full`],
|
||||
cert: undefined,
|
||||
actionsEnabled: undefined,
|
||||
action: undefined
|
||||
actionsEnabled: undefined
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
// simply use the api helper since we will not store the request's result.
|
||||
api.get(`domains/cert-status/${this.name}?full`).then((data) => {
|
||||
const certData = data.certificates[this.name]
|
||||
|
||||
const cert = {
|
||||
type: certData.CA_type.verbose,
|
||||
name: certData.CA_name,
|
||||
validity: certData.validity,
|
||||
acmeEligible: certData.ACME_eligible
|
||||
}
|
||||
|
||||
switch (certData.summary.code) {
|
||||
case 'critical':
|
||||
cert.alert = { type: 'danger', trad: 'not_valid', icon: 'exclamation-circle' }
|
||||
break
|
||||
case 'warning':
|
||||
cert.alert = { type: 'warning', trad: 'selfsigned', icon: 'exclamation-triangle' }
|
||||
break
|
||||
case 'attention':
|
||||
if (cert.type === 'lets-encrypt') {
|
||||
cert.alert = { type: 'warning', trad: 'letsencrypt_about_to_expire', icon: 'clock-o' }
|
||||
} else {
|
||||
cert.alert = { type: 'danger', trad: 'about_to_expire', icon: 'clock-o' }
|
||||
}
|
||||
break
|
||||
case 'good':
|
||||
cert.alert = { type: 'success', trad: 'good', icon: 'check-circle' }
|
||||
break
|
||||
case 'great':
|
||||
cert.alert = { type: 'success', trad: 'great', icon: 'thumbs-up' }
|
||||
break
|
||||
default:
|
||||
cert.alert = { type: 'warning', trad: 'unknown', icon: 'question' }
|
||||
}
|
||||
|
||||
const actionsEnabled = {
|
||||
installLetsencrypt: false,
|
||||
manualRenewLetsencrypt: false,
|
||||
regenSelfsigned: false,
|
||||
replaceWithSelfsigned: false
|
||||
}
|
||||
|
||||
switch (certData.CA_type.code) {
|
||||
case 'self-signed':
|
||||
actionsEnabled.installLetsencrypt = true
|
||||
actionsEnabled.regenSelfsigned = true
|
||||
break
|
||||
case 'lets-encrypt':
|
||||
actionsEnabled.manualRenewLetsencrypt = true
|
||||
actionsEnabled.replaceWithSelfsigned = true
|
||||
break
|
||||
default:
|
||||
actionsEnabled.replaceWithSelfsigned = true
|
||||
}
|
||||
|
||||
this.action = undefined
|
||||
this.cert = cert
|
||||
this.actionsEnabled = actionsEnabled
|
||||
})
|
||||
formatCertAlert (code, type) {
|
||||
switch (code) {
|
||||
case 'critical': return { type: 'danger', trad: 'not_valid', icon: 'exclamation-circle' }
|
||||
case 'warning': return { type: 'warning', trad: 'selfsigned', icon: 'exclamation-triangle' }
|
||||
case 'attention':
|
||||
if (type === 'lets-encrypt') {
|
||||
return { type: 'warning', trad: 'letsencrypt_about_to_expire', icon: 'clock-o' }
|
||||
} else {
|
||||
return { type: 'danger', trad: 'about_to_expire', icon: 'clock-o' }
|
||||
}
|
||||
case 'good': return { type: 'success', trad: 'good', icon: 'check-circle' }
|
||||
case 'great': return { type: 'success', trad: 'great', icon: 'thumbs-up' }
|
||||
default: return { type: 'warning', trad: 'unknown', icon: 'question' }
|
||||
}
|
||||
},
|
||||
|
||||
callAction () {
|
||||
const action = this.action
|
||||
formatCertData (data) {
|
||||
const certData = data.certificates[this.name]
|
||||
|
||||
const cert = {
|
||||
type: certData.CA_type.verbose,
|
||||
name: certData.CA_name,
|
||||
validity: certData.validity,
|
||||
acmeEligible: certData.ACME_eligible,
|
||||
alert: this.formatCertAlert(certData.summary.code, certData.CA_type.verbose)
|
||||
}
|
||||
|
||||
const actionsEnabled = {
|
||||
installLetsencrypt: false,
|
||||
manualRenewLetsencrypt: false,
|
||||
regenSelfsigned: false,
|
||||
replaceWithSelfsigned: false
|
||||
}
|
||||
|
||||
switch (certData.CA_type.code) {
|
||||
case 'self-signed':
|
||||
actionsEnabled.installLetsencrypt = true
|
||||
actionsEnabled.regenSelfsigned = true
|
||||
break
|
||||
case 'lets-encrypt':
|
||||
actionsEnabled.manualRenewLetsencrypt = true
|
||||
actionsEnabled.replaceWithSelfsigned = true
|
||||
break
|
||||
default:
|
||||
actionsEnabled.replaceWithSelfsigned = true
|
||||
}
|
||||
|
||||
this.cert = cert
|
||||
this.actionsEnabled = actionsEnabled
|
||||
},
|
||||
|
||||
async callAction (action) {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t(`confirm_cert_${action}`))
|
||||
if (!confirmed) return
|
||||
|
||||
let uri = 'domains/cert-install/' + this.name
|
||||
if (action === 'regen_selfsigned') uri += '?self_signed'
|
||||
else if (action === 'manual_renew_LE') uri += '?force'
|
||||
else if (action === 'revert_to_selfsigned') uri += '?self_signed&force'
|
||||
|
||||
api.post(uri, {}).then(() => this.fetchData())
|
||||
// FIXME trigger loading ? while posting ? while getting ?
|
||||
// this.$refs.view.fallback_loading = true
|
||||
api.post(uri).then(this.$refs.view.fetchQueries)
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
<template>
|
||||
<div class="domain-dns">
|
||||
<p class="alert alert-warning">
|
||||
<icon iname="warning" /> {{ $t('domain_dns_conf_is_just_a_recommendation') }}
|
||||
</p>
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="globe" /> {{ $t('domain_dns_config') }}</h2>
|
||||
</template>
|
||||
<pre><code>{{ dnsConfig }}</code></pre>
|
||||
</b-card>
|
||||
</div>
|
||||
<view-base :queries="queries" @queries-response="dnsConfig = $event" skeleton="card-info-skeleton">
|
||||
<template #top>
|
||||
<p class="alert alert-warning">
|
||||
<icon iname="warning" /> {{ $t('domain_dns_conf_is_just_a_recommendation') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<card :title="$t('domain_dns_config')" icon="globe" no-body>
|
||||
<pre class="log"><code>{{ dnsConfig }}</code></pre>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'DomainDns',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [`domains/${this.name}/dns`],
|
||||
dnsConfig: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// simply use the api helper since we will not store the request's result.
|
||||
api.get(`domains/${this.name}/dns`).then(dnsConfig => {
|
||||
this.dnsConfig = dnsConfig
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<template>
|
||||
<div class="domain-info">
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="globe" /> {{ name }}</h2>
|
||||
</template>
|
||||
<view-base :queries="queries" skeleton="card-list-skeleton">
|
||||
<card :title="name" icon="globe">
|
||||
<!-- VISIT -->
|
||||
<p>{{ $t('domain_visit_url', { url: 'https://' + name }) }}</p>
|
||||
<b-button variant="success" :href="'https://' + name" target="_blank">
|
||||
|
@ -13,16 +10,12 @@
|
|||
|
||||
<!-- DEFAULT DOMAIN -->
|
||||
<p>{{ $t('domain_default_desc') }}</p>
|
||||
<template v-if="isMainDomain">
|
||||
<p class="alert alert-info">
|
||||
<icon iname="star" /> {{ $t('domain_default_longdesc') }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<b-button variant="info" v-b-modal.default-domain-modal>
|
||||
<icon iname="star" /> {{ $t('set_default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
<p v-if="isMainDomain" class="alert alert-info">
|
||||
<icon iname="star" /> {{ $t('domain_default_longdesc') }}
|
||||
</p>
|
||||
<b-button v-else variant="info" @click="setAsDefaultDomain">
|
||||
<icon iname="star" /> {{ $t('set_default') }}
|
||||
</b-button>
|
||||
<hr>
|
||||
|
||||
<!-- DNS CONFIG -->
|
||||
|
@ -41,53 +34,50 @@
|
|||
|
||||
<!-- DELETE -->
|
||||
<p>{{ $t('domain_delete_longdesc') }}</p>
|
||||
<b-button variant="danger" v-b-modal.delete-modal>
|
||||
<p
|
||||
v-if="isMainDomain" class="alert alert-danger"
|
||||
v-html="$t('domain_delete_forbidden_desc', { domain: name })"
|
||||
/>
|
||||
<b-button v-else variant="danger" @click="deleteDomain">
|
||||
<icon iname="trash-o" /> {{ $t('delete') }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
|
||||
<!-- DEFAULT DOMAIN MODAL -->
|
||||
<b-modal
|
||||
id="default-domain-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="setAsDefaultDomain" hide-header
|
||||
>
|
||||
{{ $t('confirm_change_maindomain') }}
|
||||
</b-modal>
|
||||
|
||||
<!-- DELETE MODAL -->
|
||||
<b-modal
|
||||
id="delete-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="deleteDomain" hide-header
|
||||
>
|
||||
{{ $t('confirm_delete', { name }) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'DomainInfo',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [{ uri: 'domains/main', storeKey: 'main_domain' }]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
mainDomain () {
|
||||
return this.$store.state.data.main_domain
|
||||
},
|
||||
...mapGetters(['mainDomain']),
|
||||
|
||||
isMainDomain () {
|
||||
if (!this.mainDomain) return
|
||||
return this.name === this.mainDomain
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
deleteDomain () {
|
||||
async deleteDomain () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: this.name }))
|
||||
if (!confirmed) return
|
||||
|
||||
this.$store.dispatch('DELETE',
|
||||
{ uri: 'domains', param: this.name }
|
||||
).then(() => {
|
||||
|
@ -95,22 +85,17 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
setAsDefaultDomain () {
|
||||
async setAsDefaultDomain () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_change_maindomain'))
|
||||
if (!confirmed) return
|
||||
|
||||
this.$store.dispatch('PUT',
|
||||
{ uri: 'domains/main', data: { new_main_domain: this.name }, storeKey: 'main_domain' }
|
||||
).then(() => {
|
||||
// FIXME have to commit here since the response's is empty
|
||||
// Have to commit by hand here since the response is empty
|
||||
this.$store.commit('UPDATE_MAIN_DOMAIN', this.name)
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('FETCH',
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' }
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<search-view
|
||||
<view-search
|
||||
id="domain-list"
|
||||
:search.sync="search"
|
||||
:items="domains"
|
||||
:filtered-items="filteredDomains"
|
||||
items-name="domains"
|
||||
:queries="queries"
|
||||
>
|
||||
<template #top-bar-buttons>
|
||||
<b-button variant="success" :to="{ name: 'domain-add' }">
|
||||
|
@ -34,19 +35,21 @@
|
|||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</search-view>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import SearchView from '@/components/SearchView'
|
||||
|
||||
export default {
|
||||
name: 'DomainList',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'domains' }
|
||||
],
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
|
@ -61,17 +64,8 @@ export default {
|
|||
const domains = this.domains
|
||||
.filter(name => name.toLowerCase().includes(search))
|
||||
.sort(prevDomain => prevDomain === mainDomain ? -1 : 1)
|
||||
return domains.length > 0 ? domains : null
|
||||
return domains.length ? domains : null
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'domains' }
|
||||
])
|
||||
},
|
||||
|
||||
components: { SearchView }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<template lang="html">
|
||||
<template>
|
||||
<card-form
|
||||
:title="$t('group_new')" icon="users"
|
||||
:validation="$v" :server-error="serverError"
|
||||
|
@ -11,22 +11,18 @@
|
|||
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
import { required, alphalownum_ } from '@/helpers/validators'
|
||||
|
||||
import { required, alphalownum_ } from '@/helpers/validators'
|
||||
|
||||
export default {
|
||||
name: 'GroupCreate',
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
||||
data () {
|
||||
return {
|
||||
form: {
|
||||
groupname: ''
|
||||
},
|
||||
|
||||
serverError: '',
|
||||
|
||||
groupname: {
|
||||
label: this.$i18n.t('group_name'),
|
||||
description: this.$i18n.t('group_format_name_help'),
|
||||
|
@ -55,6 +51,8 @@ export default {
|
|||
this.isValid.groupname = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [validationMixin]
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,146 +1,110 @@
|
|||
<template>
|
||||
<search-view
|
||||
id="group-list"
|
||||
<view-search
|
||||
items-name="groups"
|
||||
:search.sync="search"
|
||||
:items="normalGroups"
|
||||
:filtered-items="filteredGroups"
|
||||
items-name="groups"
|
||||
:queries="queries"
|
||||
@queries-response="formatGroups"
|
||||
skeleton="card-form-skeleton"
|
||||
>
|
||||
<template #top-bar-buttons>
|
||||
<b-button variant="success" :to="{ name: 'group-create' }">
|
||||
<icon iname="plus" />
|
||||
{{ $t('group_new') }}
|
||||
<icon iname="plus" /> {{ $t('group_new') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<!-- PRIMARY GROUPS CARDS -->
|
||||
<b-card
|
||||
v-for="(group, name, index) in filteredGroups" :key="name"
|
||||
no-body
|
||||
<card
|
||||
v-for="(group, name) in filteredGroups" :key="name" collapsable
|
||||
:title="group.isSpecial ? $t('group_' + name) : `${$t('group')} '${name}'`" icon="group"
|
||||
>
|
||||
<b-card-header class="d-flex align-items-center">
|
||||
<h2>
|
||||
<icon iname="group" /> {{ group.isSpecial ? $t('group_' + name) : `${$t('group')} "${name}"` }}
|
||||
</h2>
|
||||
<template #header-buttons>
|
||||
<!-- DELETE GROUP -->
|
||||
<b-button
|
||||
v-if="!group.isSpecial" @click="deleteGroup(name)"
|
||||
size="sm" variant="danger"
|
||||
>
|
||||
<icon iname="trash-o" /> {{ $t('delete') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<div class="ml-auto">
|
||||
<b-button v-b-toggle="'collapse-' + index" size="sm" variant="outline-secondary">
|
||||
<icon iname="chevron-right" class="rotate" /><span class="sr-only">{{ $t('words.collapse') }}</span>
|
||||
</b-button>
|
||||
<b-row>
|
||||
<b-col md="3" lg="2">
|
||||
<strong>{{ $t('users') }}</strong>
|
||||
</b-col>
|
||||
|
||||
<b-button
|
||||
v-if="!group.isSpecial" v-b-modal.delete-modal
|
||||
variant="danger" class="ml-2" size="sm"
|
||||
@click="groupToDelete = name"
|
||||
>
|
||||
<icon :title="$t('delete')" iname="trash-o" /> <span class="sr-only">{{ $t('delete') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
<b-col>
|
||||
<template v-if="group.isSpecial">
|
||||
<p><icon iname="info-circle" /> {{ $t('group_explain_' + name) }}</p>
|
||||
<p v-if="name === 'visitors'">
|
||||
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<zone-selectize
|
||||
:choices="group.availableMembers" :selected="group.members"
|
||||
item-icon="user"
|
||||
:label="$t('group_add_member')"
|
||||
@change="onUserChanged({ ...$event, name })"
|
||||
/>
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr>
|
||||
|
||||
<b-collapse :id="'collapse-' + index" visible>
|
||||
<b-card-body>
|
||||
<b-row>
|
||||
<b-col md="3" lg="2">
|
||||
<strong>{{ $t('users') }}</strong>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<template v-if="group.isSpecial">
|
||||
<p><icon iname="info-circle" /> {{ $t('group_explain_' + name) }}</p>
|
||||
<p v-if="name === 'visitors'">
|
||||
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<zone-selectize
|
||||
:choices="group.availableMembers" :selected="group.members"
|
||||
item-icon="user"
|
||||
:label="$t('group_add_member')"
|
||||
@change="onUserChanged({ ...$event, name })"
|
||||
/>
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr>
|
||||
<b-row>
|
||||
<b-col md="3" lg="2">
|
||||
<strong>{{ $t('permissions') }}</strong>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<zone-selectize
|
||||
item-icon="key-modern" item-variant="dark"
|
||||
:choices="group.availablePermissions"
|
||||
:selected="group.permissions"
|
||||
:label="$t('group_add_permission')"
|
||||
:format="formatPermission"
|
||||
:removable="name === 'visitors' ? removable : null"
|
||||
@change="onPermissionChanged({ ...$event, name, groupType: 'normal' })"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card-body>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
<b-row>
|
||||
<b-col md="3" lg="2">
|
||||
<strong>{{ $t('permissions') }}</strong>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<zone-selectize
|
||||
item-icon="key-modern" item-variant="dark"
|
||||
:choices="group.availablePermissions"
|
||||
:selected="group.permissions"
|
||||
:label="$t('group_add_permission')"
|
||||
:format="formatPermission"
|
||||
:removable="name === 'visitors' ? removable : null"
|
||||
@change="onPermissionChanged({ ...$event, name, groupType: 'normal' })"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</card>
|
||||
|
||||
<!-- GROUP SPECIFIC CARD -->
|
||||
<template #extra>
|
||||
<b-card no-body v-if="userGroups">
|
||||
<b-card-header class="d-flex align-items-center">
|
||||
<h2>
|
||||
<icon iname="group" /> {{ $t('group_specific_permissions') }}
|
||||
</h2>
|
||||
<card
|
||||
v-if="userGroups" collapsable
|
||||
:title="$t('group_specific_permissions')" icon="group"
|
||||
>
|
||||
<template v-for="(name, index) in userGroupsNames">
|
||||
<b-row :key="name">
|
||||
<b-col md="3" lg="2">
|
||||
<icon iname="user" /> <strong>{{ name }}</strong>
|
||||
</b-col>
|
||||
|
||||
<div class="ml-auto">
|
||||
<b-button v-b-toggle.collapse-specific size="sm" variant="outline-secondary">
|
||||
<icon iname="chevron-right" class="rotate" /><span class="sr-only">{{ $t('words.collapse') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-collapse id="collapse-specific" visible>
|
||||
<b-card-body>
|
||||
<div v-for="name in userGroupsNames" :key="name">
|
||||
<b-row>
|
||||
<b-col md="3" lg="2">
|
||||
<icon iname="user" /> <strong>{{ name }}</strong>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<zone-selectize
|
||||
item-icon="key-modern" item-variant="dark"
|
||||
:choices="userGroups[name].availablePermissions"
|
||||
:selected="userGroups[name].permissions"
|
||||
:label="$t('group_add_permission')"
|
||||
:format="formatPermission"
|
||||
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<base-selectize
|
||||
v-if="availableMembers.length"
|
||||
:label="$t('group_add_member')"
|
||||
:choices="availableMembers"
|
||||
:selected="userGroupsNames"
|
||||
@selected="onSpecificUserAdded"
|
||||
<b-col>
|
||||
<zone-selectize
|
||||
item-icon="key-modern" item-variant="dark"
|
||||
:choices="userGroups[name].availablePermissions"
|
||||
:selected="userGroups[name].permissions"
|
||||
:label="$t('group_add_permission')"
|
||||
:format="formatPermission"
|
||||
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
|
||||
/>
|
||||
</b-card-body>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr :key="index">
|
||||
</template>
|
||||
|
||||
<!-- DELETE GROUP MODAL -->
|
||||
<b-modal
|
||||
v-if="groupToDelete" id="delete-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="deleteGroup" hide-header
|
||||
>
|
||||
{{ $t('confirm_delete', {name: groupToDelete }) }}
|
||||
</b-modal>
|
||||
</template>
|
||||
</search-view>
|
||||
<base-selectize
|
||||
v-if="availableMembers.length"
|
||||
:label="$t('group_add_member')"
|
||||
:choices="availableMembers"
|
||||
:selected="userGroupsNames"
|
||||
@selected="onSpecificUserAdded"
|
||||
/>
|
||||
</card>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -148,7 +112,6 @@ import Vue from 'vue'
|
|||
|
||||
import api from '@/api'
|
||||
import { isEmptyValue } from '@/helpers/commons'
|
||||
import SearchView from '@/components/SearchView'
|
||||
import ZoneSelectize from '@/components/ZoneSelectize'
|
||||
import BaseSelectize from '@/components/BaseSelectize'
|
||||
|
||||
|
@ -159,11 +122,15 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
{ uri: 'users' },
|
||||
{ uri: 'users/groups?full&include_primary_groups', storeKey: 'groups' },
|
||||
{ uri: 'users/permissions?full', storeKey: 'permissions' }
|
||||
],
|
||||
search: '',
|
||||
permissions: undefined,
|
||||
normalGroups: undefined,
|
||||
userGroups: undefined,
|
||||
groupToDelete: undefined
|
||||
userGroups: undefined
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -199,58 +166,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onPermissionChanged ({ item, index, name, groupType, action }) {
|
||||
const uri = 'users/permissions/' + item
|
||||
const data = { [action]: name }
|
||||
const from = action === 'add' ? 'availablePermissions' : 'permissions'
|
||||
const to = action === 'add' ? 'permissions' : 'availablePermissions'
|
||||
api.put(uri, data).then(() => {
|
||||
this[groupType + 'Groups'][name][from].splice(index, 1)
|
||||
this[groupType + 'Groups'][name][to].push(item)
|
||||
})
|
||||
},
|
||||
|
||||
onUserChanged ({ item, index, name, action }) {
|
||||
const uri = 'users/groups/' + name
|
||||
const data = { [action]: item }
|
||||
const from = action === 'add' ? 'availableMembers' : 'members'
|
||||
const to = action === 'add' ? 'members' : 'availableMembers'
|
||||
api.put(uri, data).then(() => {
|
||||
this.normalGroups[name][from].splice(index, 1)
|
||||
this.normalGroups[name][to].push(item)
|
||||
})
|
||||
},
|
||||
|
||||
onSpecificUserAdded ({ item }) {
|
||||
this.userGroups[item].permissions = []
|
||||
},
|
||||
|
||||
// FIXME Find a way to pass a filter to a component
|
||||
formatPermission (name) {
|
||||
return this.permissions[name].label
|
||||
},
|
||||
|
||||
removable (name) {
|
||||
return this.permissions[name].protected === false
|
||||
},
|
||||
|
||||
deleteGroup () {
|
||||
const groupname = this.groupToDelete
|
||||
this.$store.dispatch('DELETE',
|
||||
{ uri: 'users/groups', param: groupname, storeKey: 'groups' }
|
||||
).then(() => {
|
||||
Vue.delete(this.groups, groupname)
|
||||
})
|
||||
this.groupToDelete = undefined
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'users' },
|
||||
{ uri: 'users/groups?full&include_primary_groups', storeKey: 'groups' },
|
||||
{ uri: 'users/permissions?full', storeKey: 'permissions' }
|
||||
]).then(([users, allGroups, permissions]) => {
|
||||
formatGroups (users, allGroups, permissions) {
|
||||
// Do not use computed properties to get values from the store here to avoid auto
|
||||
// updates while modifying values.
|
||||
const normalGroups = {}
|
||||
|
@ -289,12 +205,57 @@ export default {
|
|||
|
||||
this.permissions = permissions
|
||||
this.normalGroups = normalGroups
|
||||
this.userGroups = userGroups
|
||||
})
|
||||
this.userGroups = isEmptyValue(userGroups) ? null : userGroups
|
||||
},
|
||||
|
||||
onPermissionChanged ({ item, index, name, groupType, action }) {
|
||||
const uri = 'users/permissions/' + item
|
||||
const data = { [action]: name }
|
||||
const from = action === 'add' ? 'availablePermissions' : 'permissions'
|
||||
const to = action === 'add' ? 'permissions' : 'availablePermissions'
|
||||
api.put(uri, data).then(() => {
|
||||
this[groupType + 'Groups'][name][from].splice(index, 1)
|
||||
this[groupType + 'Groups'][name][to].push(item)
|
||||
})
|
||||
},
|
||||
|
||||
onUserChanged ({ item, index, name, action }) {
|
||||
const uri = 'users/groups/' + name
|
||||
const data = { [action]: item }
|
||||
const from = action === 'add' ? 'availableMembers' : 'members'
|
||||
const to = action === 'add' ? 'members' : 'availableMembers'
|
||||
api.put(uri, data).then(() => {
|
||||
this.normalGroups[name][from].splice(index, 1)
|
||||
this.normalGroups[name][to].push(item)
|
||||
})
|
||||
},
|
||||
|
||||
onSpecificUserAdded ({ item }) {
|
||||
this.userGroups[item].permissions = []
|
||||
},
|
||||
|
||||
// FIXME Find a way to pass a filter to a component
|
||||
formatPermission (name) {
|
||||
return this.permissions[name].label
|
||||
},
|
||||
|
||||
removable (name) {
|
||||
return this.permissions[name].protected === false
|
||||
},
|
||||
|
||||
async deleteGroup (name) {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name }))
|
||||
if (!confirmed) return
|
||||
|
||||
this.$store.dispatch('DELETE',
|
||||
{ uri: 'users/groups', param: name, storeKey: 'groups' }
|
||||
).then(() => {
|
||||
Vue.delete(this.normalGroups, name)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
SearchView,
|
||||
ZoneSelectize,
|
||||
BaseSelectize
|
||||
}
|
||||
|
|
|
@ -1,91 +1,71 @@
|
|||
<template>
|
||||
<div class="service-info">
|
||||
<view-base
|
||||
:queries="queries" @queries-response="formatServiceData"
|
||||
ref="view" skeleton="card-info-skeleton"
|
||||
>
|
||||
<!-- INFO CARD -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<div class="d-sm-flex">
|
||||
<h2><icon iname="info-circle" /> {{ name }}</h2>
|
||||
<div class="ml-auto mt-2 mt-sm-0">
|
||||
<template v-if="status === 'running'">
|
||||
<b-button variant="warning" @click="action = 'restart'" v-b-modal.action-confirm-modal>
|
||||
<icon iname="refresh" /> {{ $t('restart') }}
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="!critical" variant="danger" class="ml-2"
|
||||
@click="action = 'stop'" v-b-modal.action-confirm-modal
|
||||
>
|
||||
<icon iname="warning" /> {{ $t('stop') }}
|
||||
</b-button>
|
||||
</template>
|
||||
<b-button
|
||||
v-else
|
||||
variant="success" @click="action = 'start'" v-b-modal.action-confirm-modal
|
||||
>
|
||||
<icon iname="play" /> {{ $t('start') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<card :title="name" icon="info-circle" button-unbreak="sm">
|
||||
<template #header-buttons>
|
||||
<template v-if="infos.status === 'running'">
|
||||
<!-- RESTART SERVICE -->
|
||||
<b-button @click="updateService('restart')" variant="warning">
|
||||
<icon iname="refresh" /> {{ $t('restart') }}
|
||||
</b-button>
|
||||
|
||||
<!-- STOP SERVICE -->
|
||||
<b-button v-if="!isCritical" @click="updateService('stop')" variant="danger">
|
||||
<icon iname="warning" /> {{ $t('stop') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<!-- START SERVICE -->
|
||||
<b-button v-else @click="updateService('start')" variant="success">
|
||||
<icon iname="play" /> {{ $t('start') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col cols="auto" md="3"><strong v-t="'description'" /></b-col>
|
||||
<b-col>{{ description }}</b-col>
|
||||
</b-row>
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col cols="auto" md="3"><strong v-t="'status'" /></b-col>
|
||||
<b-row
|
||||
v-for="(value, key) in infos" :key="key"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col md="3" xl="2">
|
||||
<strong>{{ $t(key === 'start_on_boot' ? 'service_' + key : key) }}</strong>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span :class="status === 'running' ? 'text-success' : 'text-danger'">
|
||||
<icon :iname="status === 'running' ? 'check-circle' : 'times'" />
|
||||
{{ $t(status) }}
|
||||
<template v-if="key === 'status'">
|
||||
<span :class="value === 'running' ? 'text-success' : 'text-danger'">
|
||||
<icon :iname="value === 'running' ? 'check-circle' : 'times'" />
|
||||
{{ $t(value) }}
|
||||
</span>
|
||||
{{ $t('since') }} {{ uptime | distanceToNow }}
|
||||
</template>
|
||||
|
||||
<span v-else-if="key === 'start_on_boot'" :class="value === 'enabled' ? 'text-success' : 'text-danger'">
|
||||
{{ $t(value) }}
|
||||
</span>
|
||||
{{ $t('since') }} {{ last_state_change | distanceToNow }}
|
||||
|
||||
<span v-else v-t="value" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col cols="auto" md="3"><strong v-t="'service_start_on_boot'" /></b-col>
|
||||
<b-col>
|
||||
<span :class="start_on_boot === 'enabled' ? 'text-success' : 'text-danger'">
|
||||
{{ $t(start_on_boot) }}
|
||||
</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row no-gutters class="row-line">
|
||||
<b-col cols="auto" md="3"><strong v-t="'configuration'" /></b-col>
|
||||
<b-col>
|
||||
<span :class="{ 'text-success': configuration === 'valid', 'text-danger': configuration === 'broken' }">
|
||||
{{ $t(configuration) }}
|
||||
</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- LOGS CARD -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<div class="d-sm-flex justify-content-sm-between">
|
||||
<h2><icon iname="book" /> {{ $t('logs') }}</h2>
|
||||
<b-button variant="success" @click="shareLogs" class="mt-2 mt-sm-0">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<card :title="$t('logs')" icon="book" button-unbreak="sm">
|
||||
<template #header-buttons>
|
||||
<b-button variant="success" @click="shareLogs">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<div class="w-100" v-for="{ filename, content} in logs" :key="filename">
|
||||
<h3>{{ filename }}</h3>
|
||||
<pre class="bg-light p-3"><code>{{ content }}</code></pre>
|
||||
</div>
|
||||
</b-card>
|
||||
<template v-for="({ filename, content }, i) in logs">
|
||||
<h3 :key="i + '-filename'">
|
||||
{{ filename }}
|
||||
</h3>
|
||||
|
||||
<!-- ACTIONS CONFIRMATION MODAL -->
|
||||
<b-modal
|
||||
v-if="action"
|
||||
id="action-confirm-modal" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="updateService" hide-header
|
||||
>
|
||||
{{ $t(`confirm_service_${action}`, { name }) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<pre :key="i + '-content'" class="log"><code>{{ content }}</code></pre>
|
||||
</template>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -96,68 +76,58 @@ export default {
|
|||
name: 'ServiceInfo',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
'services/' + this.name,
|
||||
`services/${this.name}/log?number=50`
|
||||
],
|
||||
// Service data
|
||||
status: undefined,
|
||||
description: '',
|
||||
configuration: '',
|
||||
last_state_change: 0,
|
||||
start_on_boot: undefined,
|
||||
infos: undefined,
|
||||
uptime: undefined,
|
||||
isCritical: undefined,
|
||||
logs: undefined,
|
||||
// Modal action
|
||||
action: undefined,
|
||||
critical: undefined
|
||||
action: undefined
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
distanceToNow
|
||||
},
|
||||
|
||||
computed: {
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
// simply use the api helper since we will not store the request's result.
|
||||
api.getAll([
|
||||
'services/' + this.name,
|
||||
`services/${this.name}/log?number=50`
|
||||
]).then(([service, logs]) => {
|
||||
this.critical = ['nginx', 'ssh', 'slapd', 'yunohost-api'].includes(this.name)
|
||||
if (service.last_state_change === 'unknown') {
|
||||
service.last_state_change = 0
|
||||
}
|
||||
for (const key in service) {
|
||||
this[key] = service[key]
|
||||
}
|
||||
this.logs = Object.keys(logs).sort((prev, curr) => {
|
||||
if (prev === 'journalctl') return -1
|
||||
else if (curr === 'journalctl') return 1
|
||||
else if (prev < curr) return -1
|
||||
else return 1
|
||||
}).map(filename => ({ content: logs[filename].join('\n'), filename }))
|
||||
})
|
||||
formatServiceData (
|
||||
// eslint-disable-next-line
|
||||
{ status, description, start_on_boot, last_state_change, configuration },
|
||||
logs
|
||||
) {
|
||||
this.isCritical = ['nginx', 'ssh', 'slapd', 'yunohost-api'].includes(this.name)
|
||||
// eslint-disable-next-line
|
||||
this.uptime = last_state_change === 'unknown' ? 0 : last_state_change
|
||||
this.infos = { description, status, start_on_boot, configuration }
|
||||
|
||||
this.logs = Object.keys(logs).sort((prev, curr) => {
|
||||
if (prev === 'journalctl') return -1
|
||||
else if (curr === 'journalctl') return 1
|
||||
else if (prev < curr) return -1
|
||||
else return 1
|
||||
}).map(filename => ({ content: logs[filename].join('\n'), filename }))
|
||||
},
|
||||
|
||||
updateService () {
|
||||
if (!['start', 'restart', 'stop'].includes(this.action)) return
|
||||
const method = this.action === 'stop' ? 'delete' : 'put'
|
||||
const uri = this.action === 'restart'
|
||||
async updateService (action) {
|
||||
const confirmed = await this.$askConfirmation(
|
||||
this.$i18n.t('confirm_service_' + action, { name: this.name })
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
if (!['start', 'restart', 'stop'].includes(action)) return
|
||||
const method = action === 'stop' ? 'delete' : 'put'
|
||||
const uri = action === 'restart'
|
||||
? `services/${this.name}/restart`
|
||||
: 'services/' + this.name
|
||||
|
||||
// FIXME API doesn't return anything to the PUT so => json err
|
||||
api[method](uri).then(() => {
|
||||
this.fetchData()
|
||||
})
|
||||
api[method](uri).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
shareLogs () {
|
||||
|
@ -178,11 +148,16 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
filters: { distanceToNow }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
<template>
|
||||
<search-view
|
||||
<view-search
|
||||
id="service-list"
|
||||
:search.sync="search"
|
||||
:items="services"
|
||||
:filtered-items="filteredServices"
|
||||
items-name="services"
|
||||
:queries="queries"
|
||||
@queries-response="formatServices"
|
||||
>
|
||||
<b-list-group v-if="filteredServices">
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="{ name, description, status, last_state_change } in filteredServices"
|
||||
:key="name || service"
|
||||
v-for="{ name, description, status, last_state_change } in filteredServices" :key="name"
|
||||
:to="{ name: 'service-info', params: { name }}"
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div class="w-100">
|
||||
<div>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ name }}
|
||||
<small class="text-secondary">{{ description }}</small>
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
<p class="m-0">
|
||||
<span :class="status === 'running' ? 'text-success' : 'text-danger'">
|
||||
<icon :iname="status === 'running' ? 'check-circle' : 'times'" />
|
||||
{{ $t(status) }}
|
||||
|
@ -26,22 +27,22 @@
|
|||
{{ $t('since') }} {{ last_state_change | distanceToNow }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</search-view>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { distanceToNow } from '@/helpers/filters/date'
|
||||
import SearchView from '@/components/SearchView'
|
||||
|
||||
export default {
|
||||
name: 'ServiceList',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: ['services'],
|
||||
search: '',
|
||||
services: undefined
|
||||
}
|
||||
|
@ -54,41 +55,31 @@ export default {
|
|||
const services = this.services.filter(({ name }) => {
|
||||
return name.toLowerCase().includes(search)
|
||||
})
|
||||
return services.length > 0 ? services : null
|
||||
return services.length ? services : null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
// simply use the api helper since we will not store the request's result.
|
||||
api.get('services').then(servicesData => {
|
||||
this.services = Object.keys(servicesData).sort().map(name => {
|
||||
const service = servicesData[name]
|
||||
if (service.last_state_change === 'unknown') {
|
||||
service.last_state_change = 0
|
||||
}
|
||||
return { ...service, name }
|
||||
})
|
||||
formatServices (services) {
|
||||
this.services = Object.keys(services).sort().map(name => {
|
||||
const service = services[name]
|
||||
if (service.last_state_change === 'unknown') {
|
||||
service.last_state_change = 0
|
||||
}
|
||||
return { ...service, name }
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
components: { SearchView },
|
||||
|
||||
filters: {
|
||||
distanceToNow
|
||||
}
|
||||
filters: { distanceToNow }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include media-breakpoint-down(md) {
|
||||
h5 small {
|
||||
display: block;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
:server-error="serverError"
|
||||
@submit="onSubmit"
|
||||
:extra="extra"
|
||||
>
|
||||
<template #extra-fields="{ v, fields, form }">
|
||||
<!-- CURRENT ADMIN PASSWORD -->
|
||||
<form-field v-bind="fields.currentPassword" v-model="form.currentPassword" :validation="v.form.currentPassword" />
|
||||
<hr>
|
||||
</template>
|
||||
</password-form>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -20,7 +14,6 @@ import { validationMixin } from 'vuelidate'
|
|||
import { PasswordForm } from '@/components/reusableForms'
|
||||
import { required, minLength } from '@/helpers/validators'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ToolAdminpw',
|
||||
|
||||
|
@ -67,9 +60,6 @@ export default {
|
|||
},
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
||||
components: {
|
||||
PasswordForm
|
||||
}
|
||||
components: { PasswordForm }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div class="tool-log">
|
||||
<view-base
|
||||
:queries="queries" @queries-response="formatFirewallData"
|
||||
ref="view" skeleton="card-form-skeleton"
|
||||
>
|
||||
<!-- PORTS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="shield" /> {{ $t('ports') }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="$t('ports')" icon="shield">
|
||||
<div v-for="(items, protocol) in protocols" :key="protocol">
|
||||
<h5>{{ $t(protocol) }}</h5>
|
||||
|
||||
<b-table
|
||||
:fields="fields" :items="items"
|
||||
small striped responsive="true"
|
||||
small striped responsive
|
||||
>
|
||||
<!-- PORT CELL -->
|
||||
<template v-slot:cell(port)="data">
|
||||
<template #cell(port)="data">
|
||||
{{ data.value }}
|
||||
</template>
|
||||
|
||||
<!-- CONNECTIONS CELL -->
|
||||
<template v-slot:cell()="data">
|
||||
<template #cell()="data">
|
||||
<b-checkbox
|
||||
v-if="data.field.key !== 'uPnP'"
|
||||
class="on-off-switch"
|
||||
v-model="data.value"
|
||||
switch
|
||||
@change="onToggle(protocol, data.field.key, data.item.port, data.index, $event)"
|
||||
@change="onTablePortToggling(data.item.port, protocol, data.field.key, data.index, $event)"
|
||||
>
|
||||
<span :class="'btn btn-sm py-0 btn-' + (data.value ? 'danger' : 'success')">
|
||||
{{ $t(data.value ? 'close' : 'open') }}
|
||||
|
@ -39,108 +39,69 @@
|
|||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- OPERATIONS -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="cogs" /> {{ $t('operations') }}</h2>
|
||||
</template>
|
||||
|
||||
<b-form
|
||||
id="port-form" inline class="d-flex justify-content-between"
|
||||
@submit.prevent="onFormSubmit"
|
||||
>
|
||||
<b-input-group :prepend="$t('action')">
|
||||
<b-select
|
||||
id="input-action"
|
||||
v-model="form.action" :options="actionChoices"
|
||||
/>
|
||||
</b-input-group>
|
||||
<card-form
|
||||
:title="$t('operations')" icon="cogs"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="onFormPortToggling"
|
||||
inline form-classes="d-flex justify-content-between align-items-start"
|
||||
>
|
||||
<b-input-group :prepend="$t('action')">
|
||||
<b-select v-model="form.action" :options="actionChoices" />
|
||||
</b-input-group>
|
||||
|
||||
<form-field :validation="$v.form.port">
|
||||
<b-input-group :prepend="$t('port')">
|
||||
<b-input
|
||||
id="input-port" placeholder="0"
|
||||
type="number" min="0" max="65535"
|
||||
v-model.number="form.port"
|
||||
<input-item
|
||||
id="input-port" placeholder="0" type="number"
|
||||
v-model="form.port"
|
||||
/>
|
||||
</b-input-group>
|
||||
</form-field>
|
||||
|
||||
<b-input-group :prepend="$t('connection')">
|
||||
<b-select
|
||||
id="input-connection"
|
||||
v-model="form.connection" :options="connectionChoices"
|
||||
/>
|
||||
</b-input-group>
|
||||
<b-input-group :prepend="$t('connection')">
|
||||
<b-select v-model="form.connection" :options="connectionChoices" id="input-connection" />
|
||||
</b-input-group>
|
||||
|
||||
<b-input-group :prepend="$t('protocol')">
|
||||
<b-select
|
||||
id="input-protocol"
|
||||
v-model="form.protocol" :options="protocolChoices"
|
||||
/>
|
||||
</b-input-group>
|
||||
</b-form>
|
||||
|
||||
<template v-slot:footer>
|
||||
<b-button type="submit" form="port-form" variant="success">
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-card>
|
||||
<b-input-group :prepend="$t('protocol')">
|
||||
<b-select v-model="form.protocol" :options="protocolChoices" id="input-protocol" />
|
||||
</b-input-group>
|
||||
</card-form>
|
||||
|
||||
<!-- UPnP -->
|
||||
<b-card :body-text-variant="upnpEnabled ? 'success' : 'danger'">
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="exchange" /> {{ $t('upnp') }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="$t('upnp')" icon="exchange" :body-text-variant="upnpEnabled ? 'success' : 'danger'">
|
||||
{{ $t(upnpEnabled ? 'upnp_enabled' : 'upnp_disabled' ) }}
|
||||
|
||||
<b-form-invalid-feedback :state="upnpError !== '' ? false : null">
|
||||
{{ upnpError }}
|
||||
</b-form-invalid-feedback>
|
||||
|
||||
<template v-slot:footer>
|
||||
<b-button
|
||||
:variant="!upnpEnabled ? 'success' : 'danger'"
|
||||
v-b-modal.toggle-upnp-modal
|
||||
>
|
||||
<template #buttons>
|
||||
<b-button @click="toggleUpnp" :variant="!upnpEnabled ? 'success' : 'danger'">
|
||||
{{ $t(!upnpEnabled ? 'enable' : 'disable' ) }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-card>
|
||||
|
||||
<!-- TOGGLE PORT CONFIRM MODAL -->
|
||||
<b-modal
|
||||
no-close-on-backdrop centered hide-header
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="togglePort(portToToggle)" ref="modal"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
{{ portToToggle ? $t('confirm_firewall_' + portToToggle.action, portToToggle) : '' }}
|
||||
</b-modal>
|
||||
|
||||
<!-- TOGGLE UPNP CONFIRM MODAL -->
|
||||
<b-modal
|
||||
id="toggle-upnp-modal"
|
||||
no-close-on-backdrop centered hide-header
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="toggleUpnp(!upnpEnabled)"
|
||||
>
|
||||
{{ $t('confirm_upnp_' + (upnpEnabled ? 'disable' : 'enable')) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import api from '@/api'
|
||||
import { required, integer, between } from '@/helpers/validators'
|
||||
|
||||
export default {
|
||||
name: 'ToolFirewall',
|
||||
|
||||
data () {
|
||||
return {
|
||||
// Tables data
|
||||
queries: ['/firewall?raw'],
|
||||
serverError: '',
|
||||
|
||||
// Ports tables data
|
||||
fields: [
|
||||
{ key: 'port', label: this.$i18n.t('port') },
|
||||
{ key: 'ipv4', label: this.$i18n.t('ipv4') },
|
||||
|
@ -150,7 +111,7 @@ export default {
|
|||
protocols: undefined,
|
||||
portToToggle: undefined,
|
||||
|
||||
// Form data
|
||||
// Ports form data
|
||||
actionChoices: [
|
||||
{ value: 'open', text: this.$i18n.t('open') },
|
||||
{ value: 'close', text: this.$i18n.t('close') }
|
||||
|
@ -177,88 +138,93 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
validations: {
|
||||
form: {
|
||||
port: { number: required, integer, between: between(0, 65535) }
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get('/firewall?raw').then(data => {
|
||||
const ports = Object.values(data).reduce((ports, protocols) => {
|
||||
for (const type of ['TCP', 'UDP']) {
|
||||
for (const port of protocols[type]) {
|
||||
ports[type].add(port)
|
||||
}
|
||||
formatFirewallData (data) {
|
||||
const ports = Object.values(data).reduce((ports, protocols) => {
|
||||
for (const type of ['TCP', 'UDP']) {
|
||||
for (const port of protocols[type]) {
|
||||
ports[type].add(port)
|
||||
}
|
||||
return ports
|
||||
}, { TCP: new Set(), UDP: new Set() })
|
||||
|
||||
const tables = {
|
||||
TCP: [],
|
||||
UDP: []
|
||||
}
|
||||
for (const protocol of ['TCP', 'UDP']) {
|
||||
for (const port of ports[protocol]) {
|
||||
const row = { port }
|
||||
for (const connection of ['ipv4', 'ipv6', 'uPnP']) {
|
||||
row[connection] = data[connection][protocol].includes(port)
|
||||
}
|
||||
tables[protocol].push(row)
|
||||
return ports
|
||||
}, { TCP: new Set(), UDP: new Set() })
|
||||
|
||||
const tables = {
|
||||
TCP: [],
|
||||
UDP: []
|
||||
}
|
||||
for (const protocol of ['TCP', 'UDP']) {
|
||||
for (const port of ports[protocol]) {
|
||||
const row = { port }
|
||||
for (const connection of ['ipv4', 'ipv6', 'uPnP']) {
|
||||
row[connection] = data[connection][protocol].includes(port)
|
||||
}
|
||||
tables[protocol].sort((a, b) => a.port < b.port ? -1 : 1)
|
||||
tables[protocol].push(row)
|
||||
}
|
||||
tables[protocol].sort((a, b) => a.port < b.port ? -1 : 1)
|
||||
}
|
||||
|
||||
this.protocols = tables
|
||||
this.upnpEnabled = data.uPnP.enabled
|
||||
this.protocols = tables
|
||||
this.upnpEnabled = data.uPnP.enabled
|
||||
},
|
||||
|
||||
togglePort ({ action, port, protocol, connection }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$askConfirmation(
|
||||
this.$i18n.t('confirm_firewall_' + action, { port, protocol, connection })
|
||||
).then(confirmed => {
|
||||
if (confirmed) {
|
||||
const method = action === 'open' ? 'post' : 'delete'
|
||||
api[method](`/firewall/port?${connection}_only`, { port, protocol }).then(() => {
|
||||
resolve(confirmed)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
} else {
|
||||
resolve(confirmed)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
togglePort ({ port, protocol, connection, action, index }) {
|
||||
const method = action === 'open' ? 'post' : 'delete'
|
||||
api[method](`/firewall/port?${connection}_only`, { port, protocol }).then(() => {
|
||||
if (index === -1) this.fetchData()
|
||||
this.portToToggle = undefined
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
async toggleUpnp (value) {
|
||||
const action = this.upnpEnabled ? 'disable' : 'enable'
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_upnp_' + action))
|
||||
if (!confirmed) return
|
||||
|
||||
toggleUpnp (value) {
|
||||
api.get('firewall/upnp?action=' + (value ? 'enable' : 'disable')).then(r => {
|
||||
api.get('firewall/upnp?action=' + action).then(() => {
|
||||
// FIXME Couldn't test when it works.
|
||||
this.fetchData()
|
||||
this.$refs.view.fetchQueries()
|
||||
}).catch(err => {
|
||||
this.upnpError = err.message
|
||||
})
|
||||
},
|
||||
|
||||
onCancel () {
|
||||
const { protocol, index, connection, value } = this.portToToggle
|
||||
if (index > -1) {
|
||||
this.$set(this.protocols[protocol][index], connection, !value)
|
||||
}
|
||||
this.portToToggle = undefined
|
||||
},
|
||||
|
||||
onToggle (protocol, connection, port, index, value) {
|
||||
onTablePortToggling (port, protocol, connection, index, value) {
|
||||
this.$set(this.protocols[protocol][index], connection, value)
|
||||
this.portToToggle = {
|
||||
protocol, connection, port, action: value ? 'open' : 'close', index, value
|
||||
}
|
||||
this.$refs.modal.show()
|
||||
const action = value ? 'open' : 'close'
|
||||
this.togglePort({ action, port, protocol, connection }).then(toggled => {
|
||||
// Revert change on cancel
|
||||
if (!toggled) {
|
||||
this.$set(this.protocols[protocol][index], connection, !value)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onFormSubmit (e) {
|
||||
// IMPROVEMENT: could check if ports are already opened for known ports (tricky with protocol='Both')
|
||||
this.portToToggle = {
|
||||
...this.form,
|
||||
value: this.form.action === 'open',
|
||||
// set index to -1 to trigger `this.fetchData` at modal `@ok`
|
||||
index: -1
|
||||
}
|
||||
this.$refs.modal.show()
|
||||
onFormPortToggling (e) {
|
||||
this.togglePort(this.form).then(toggled => {
|
||||
if (toggled) this.$refs.view.fetchQueries()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
mixins: [validationMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -293,16 +259,17 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
form {
|
||||
::v-deep form {
|
||||
margin-bottom: -1rem;
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1rem
|
||||
& > * {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
fieldset {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,38 +1,33 @@
|
|||
<!-- FIXME make a component shared with Home.vue ? -->
|
||||
<template>
|
||||
<div class="tools-menu">
|
||||
<b-list-group class="menu-list">
|
||||
<b-list-group-item
|
||||
v-for="item in menu"
|
||||
:key="item.id"
|
||||
:to="{name: item.routeName}"
|
||||
>
|
||||
<icon :iname="item.icon" class="lg" />
|
||||
<h2>{{ $t(item.translation) }}</h2>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
<b-list-group class="menu-list">
|
||||
<b-list-group-item
|
||||
v-for="item in menu"
|
||||
:key="item.routeName"
|
||||
:to="{name: item.routeName}"
|
||||
>
|
||||
<icon :iname="item.icon" class="lg" />
|
||||
<h2>{{ $t(item.translation) }}</h2>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ToolList',
|
||||
|
||||
data: () => {
|
||||
data () {
|
||||
return {
|
||||
menu: [
|
||||
{ id: 0, routeName: 'tool-logs', icon: 'book', translation: 'logs' },
|
||||
{ id: 1, routeName: 'tool-migrations', icon: 'share', translation: 'migrations' },
|
||||
{ id: 2, routeName: 'tool-firewall', icon: 'shield', translation: 'firewall' },
|
||||
{ id: 3, routeName: 'tool-adminpw', icon: 'key-modern', translation: 'tools_adminpw' },
|
||||
{ id: 4, routeName: 'tool-webadmin', icon: 'cog', translation: 'tools_webadmin_settings' },
|
||||
{ id: 5, routeName: 'tool-power', icon: 'power-off', translation: 'tools_shutdown_reboot' }
|
||||
{ routeName: 'tool-logs', icon: 'book', translation: 'logs' },
|
||||
{ routeName: 'tool-migrations', icon: 'share', translation: 'migrations' },
|
||||
{ routeName: 'tool-firewall', icon: 'shield', translation: 'firewall' },
|
||||
{ routeName: 'tool-adminpw', icon: 'key-modern', translation: 'tools_adminpw' },
|
||||
{ routeName: 'tool-webadmin', icon: 'cog', translation: 'tools_webadmin_settings' },
|
||||
{ routeName: 'tool-power', icon: 'power-off', translation: 'tools_shutdown_reboot' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<template>
|
||||
<div class="tool-log">
|
||||
<view-base
|
||||
:queries="queries" @queries-response="formatLogData"
|
||||
ref="view" skeleton="card-info-skeleton"
|
||||
>
|
||||
<!-- INFO CARD -->
|
||||
<b-card>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="info-circle" /> {{ description }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="description" icon="info-circle">
|
||||
<b-row
|
||||
v-for="(value, prop) in info" :key="prop"
|
||||
no-gutters class="row-line"
|
||||
>
|
||||
<b-col cols="auto" md="3">
|
||||
<b-col md="3" xl="2">
|
||||
<strong>{{ $t('logs_' + prop) }}</strong>
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<span v-if="prop.endsWith('_at')">{{ value | readableDate }}</span>
|
||||
|
||||
<div v-else-if="prop === 'suboperations'">
|
||||
<div v-for="operation in value" :key="operation.name">
|
||||
<icon v-if="!operation.success" iname="times" class="text-danger" />
|
||||
|
@ -23,44 +24,39 @@
|
|||
</b-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-else>{{ value }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<b-alert
|
||||
v-if="info.error" variant="danger" show
|
||||
class="my-5"
|
||||
>
|
||||
<icon iname="exclamation-circle" /> <span v-html="$t('operation_failed_explanation')" />
|
||||
</b-alert>
|
||||
<div v-if="info.error" class="alert alert-danger my-5">
|
||||
<icon iname="exclamation-circle" /> {{ $t('operation_failed_explanation') }}
|
||||
</div>
|
||||
|
||||
<!-- LOGS CARD -->
|
||||
<b-card class="log">
|
||||
<template v-slot:header>
|
||||
<div class="d-sm-flex justify-content-sm-between">
|
||||
<h2><icon iname="file-text" /> {{ $t('logs') }}</h2>
|
||||
<b-button @click="shareLogs" variant="success">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<card :title="$t('logs')" icon="file-text" no-body>
|
||||
<template #header-buttons>
|
||||
<b-button @click="shareLogs" variant="success">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<b-button
|
||||
v-if="moreLogsAvailable"
|
||||
variant="white" class="w-100 rounded-0"
|
||||
@click="fetchData"
|
||||
@click="$ref.view.fetchQueries()"
|
||||
>
|
||||
<icon iname="plus" /> {{ $t('logs_more') }}
|
||||
</b-button>
|
||||
|
||||
<pre><code v-html="logs" /></pre>
|
||||
<pre class="log"><code v-html="logs" /></pre>
|
||||
|
||||
<b-button @click="shareLogs" variant="success" class="w-100 rounded-0">
|
||||
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</div>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -72,64 +68,60 @@ export default {
|
|||
name: 'ToolLog',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// Log data
|
||||
description: '',
|
||||
description: undefined,
|
||||
info: {},
|
||||
logs: '',
|
||||
logs: undefined,
|
||||
// Logs line display
|
||||
numberOfLines: 25,
|
||||
moreLogsAvailable: false
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
readableDate
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
computed: {
|
||||
queries () {
|
||||
const queryString = objectToParams({
|
||||
path: this.name,
|
||||
filter_irrelevant: '',
|
||||
with_suboperations: '',
|
||||
number: this.numberOfLines
|
||||
})
|
||||
return ['logs/display?' + queryString]
|
||||
}
|
||||
},
|
||||
|
||||
api.get('logs/display?' + queryString).then(log => {
|
||||
if (log.logs.length === this.numberOfLines) {
|
||||
this.moreLogsAvailable = true
|
||||
this.numberOfLines *= 10
|
||||
} else {
|
||||
this.moreLogsAvailable = false
|
||||
}
|
||||
this.description = log.description
|
||||
methods: {
|
||||
formatLogData (log) {
|
||||
if (log.logs.length === this.numberOfLines) {
|
||||
this.moreLogsAvailable = true
|
||||
this.numberOfLines *= 10
|
||||
} else {
|
||||
this.moreLogsAvailable = false
|
||||
}
|
||||
this.description = log.description
|
||||
|
||||
const levels = ['ERROR', 'WARNING', 'SUCCESS', 'INFO']
|
||||
this.logs = log.logs.map(line => {
|
||||
for (const level of levels) {
|
||||
if (line.includes(level + ' -')) {
|
||||
return `<span class="alert-${level === 'ERROR'
|
||||
? 'danger'
|
||||
: level.toLowerCase()}">${line}</span>`
|
||||
}
|
||||
const levels = ['ERROR', 'WARNING', 'SUCCESS', 'INFO']
|
||||
this.logs = log.logs.map(line => {
|
||||
for (const level of levels) {
|
||||
if (line.includes(level + ' -')) {
|
||||
return `<span class="alert-${level === 'ERROR'
|
||||
? 'danger'
|
||||
: level.toLowerCase()}">${line}</span>`
|
||||
}
|
||||
return line
|
||||
}).join('\n')
|
||||
|
||||
const { started_at, ended_at, error, success, suboperations } = log.metadata
|
||||
const info = { path: log.log_path, started_at, ended_at }
|
||||
if (!success) info.error = error
|
||||
if (suboperations) info.suboperations = suboperations
|
||||
this.info = info
|
||||
})
|
||||
}
|
||||
return line
|
||||
}).join('\n')
|
||||
// eslint-disable-next-line
|
||||
const { started_at, ended_at, error, success, suboperations } = log.metadata
|
||||
const info = { path: log.log_path, started_at, ended_at }
|
||||
if (!success) info.error = error
|
||||
if (suboperations && suboperations.length) info.suboperations = suboperations
|
||||
this.info = info
|
||||
},
|
||||
|
||||
shareLogs () {
|
||||
|
@ -139,8 +131,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
}
|
||||
filters: { readableDate }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<search-view
|
||||
id="tool-logs"
|
||||
<view-search
|
||||
:search.sync="search"
|
||||
:items="operations"
|
||||
:filtered-items="filteredOperations"
|
||||
items-name="logs"
|
||||
:queries="queries"
|
||||
@queries-response="formatLogsData"
|
||||
skeleton="card-list-skeleton"
|
||||
>
|
||||
<b-card no-body>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="wrench" /> {{ $t('logs_operation') }}</h2>
|
||||
</template>
|
||||
<card :title="$t('logs_operation')" icon="wrench" no-body>
|
||||
<b-list-group flush>
|
||||
<b-list-group-item
|
||||
v-for="log in filteredOperations" :key="log.name"
|
||||
|
@ -21,20 +20,19 @@
|
|||
{{ log.description }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</search-view>
|
||||
</card>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { distanceToNow, readableDate } from '@/helpers/filters/date'
|
||||
import SearchView from '@/components/SearchView'
|
||||
|
||||
export default {
|
||||
name: 'ServiceList',
|
||||
name: 'ToolLogs',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [`logs?limit=${25}&with_details`],
|
||||
search: '',
|
||||
operations: undefined
|
||||
}
|
||||
|
@ -47,39 +45,31 @@ export default {
|
|||
const operations = this.operations.filter(({ description }) => {
|
||||
return description.toLowerCase().includes(search)
|
||||
})
|
||||
return operations.length > 0 ? operations : null
|
||||
return operations.length ? operations : null
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatLogsData ({ operation }) {
|
||||
operation.forEach((log, index) => {
|
||||
if (log.success === '?') {
|
||||
operation[index].icon = 'question'
|
||||
operation[index].class = 'warning'
|
||||
} else if (log.success) {
|
||||
operation[index].icon = 'check'
|
||||
operation[index].class = 'success'
|
||||
} else {
|
||||
operation[index].icon = 'close'
|
||||
operation[index].class = 'danger'
|
||||
}
|
||||
})
|
||||
this.operations = operation
|
||||
}
|
||||
},
|
||||
|
||||
filters: {
|
||||
distanceToNow,
|
||||
readableDate
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.get(`logs?limit=${25}&with_details`).then(({ operation }) => {
|
||||
operation.forEach((log, index) => {
|
||||
if (log.success === '?') {
|
||||
operation[index].icon = 'question'
|
||||
operation[index].class = 'warning'
|
||||
} else if (log.success) {
|
||||
operation[index].icon = 'check'
|
||||
operation[index].class = 'success'
|
||||
} else {
|
||||
operation[index].icon = 'close'
|
||||
operation[index].class = 'danger'
|
||||
}
|
||||
})
|
||||
this.operations = operation
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
components: { SearchView }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,37 +1,28 @@
|
|||
<template>
|
||||
<div class="tool-log">
|
||||
<view-base :queries="queries" @queries-response="formatMigrationsData" ref="view">
|
||||
<!-- PENDING MIGRATIONS -->
|
||||
<b-card no-body>
|
||||
<b-card-header class="d-flex align-items-center">
|
||||
<h2>
|
||||
<icon iname="cogs" /> {{ $t('migrations_pending') }}
|
||||
</h2>
|
||||
<card :title="$t('migrations_pending')" icon="cogs" no-body>
|
||||
<template #header-buttons v-if="pending">
|
||||
<b-button size="sm" variant="success" @click="runMigrations">
|
||||
<icon iname="play" /> {{ $t('run') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<div class="ml-auto" v-if="pending && pending.length">
|
||||
<b-button size="sm" variant="success" @click="runMigrations">
|
||||
<icon iname="play" /> {{ $t('run') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body v-if="pending && !pending.length">
|
||||
<b-card-body v-if="pending === null">
|
||||
<span class="text-success">
|
||||
<icon iname="check-circle" /> {{ $t('migrations_no_pending') }}
|
||||
</span>
|
||||
</b-card-body>
|
||||
|
||||
<b-list-group flush v-else-if="pending">
|
||||
<b-list-group v-else-if="pending" flush>
|
||||
<b-list-group-item
|
||||
v-for="{ number, description, id, disclaimer } in pending" :key="number"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
{{ number }}. {{ description }}
|
||||
|
||||
<div class="ml-auto" v-if="pending && pending.length">
|
||||
<b-button
|
||||
@click="skipId = id" v-b-modal.skip-modal
|
||||
size="sm" variant="warning"
|
||||
>
|
||||
<div class="ml-auto">
|
||||
<b-button @click="skipMigration(id)" size="sm" variant="warning">
|
||||
<icon iname="close" /> {{ $t('skip') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
@ -58,84 +49,67 @@
|
|||
</template>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- DONE MIGRATIONS -->
|
||||
<b-card no-body>
|
||||
<b-card-header class="d-flex align-items-center">
|
||||
<h2><icon iname="cogs" /> {{ $t('migrations_done') }}</h2>
|
||||
|
||||
<div class="ml-auto">
|
||||
<b-button v-b-toggle.collapse-done size="sm" variant="outline-secondary">
|
||||
<icon iname="chevron-right" /><span class="sr-only">{{ $t('words.collapse') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card-header>
|
||||
|
||||
<b-collapse id="collapse-done">
|
||||
<b-card-body v-if="done && !done.length">
|
||||
<span class="text-success">
|
||||
<icon iname="check-circle" /> {{ $t('migrations_no_done') }}
|
||||
</span>
|
||||
</b-card-body>
|
||||
|
||||
<b-list-group flush v-else-if="done">
|
||||
<b-list-group-item
|
||||
v-for="{ number, description } in done" :key="number"
|
||||
>
|
||||
{{ number }}. {{ description }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
|
||||
<!-- SKIP MIGRATION CONFIRMATION MODAL -->
|
||||
<b-modal
|
||||
id="skip-modal" centered
|
||||
body-bg-variant="warning"
|
||||
@ok="skipMigration" hide-header
|
||||
<card
|
||||
:title="$t('migrations_done')" icon="cogs"
|
||||
collapsable collapsed no-body
|
||||
>
|
||||
{{ $t('confirm_migrations_skip') }}
|
||||
</b-modal>
|
||||
</div>
|
||||
<b-card-body v-if="done === null">
|
||||
<span class="text-success">
|
||||
<icon iname="check-circle" /> {{ $t('migrations_no_done') }}
|
||||
</span>
|
||||
</b-card-body>
|
||||
|
||||
<b-list-group flush v-else-if="done">
|
||||
<b-list-group-item v-for="{ number, description } in done" :key="number">
|
||||
{{ number }}. {{ description }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<template #skeleton>
|
||||
<card-list-skeleton :item-count="3" />
|
||||
<b-card no-body>
|
||||
<template #header>
|
||||
<b-skeleton width="30%" height="36px" class="m-0" />
|
||||
</template>
|
||||
</b-card>
|
||||
</template>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
|
||||
// FIXME not tested with pending migrations (disclaimer and stuff)
|
||||
|
||||
export default {
|
||||
name: 'ToolMigrations',
|
||||
|
||||
props: {
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
'migrations?pending',
|
||||
'migrations?done'
|
||||
],
|
||||
pending: undefined,
|
||||
done: undefined,
|
||||
skipId: undefined,
|
||||
checked: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData () {
|
||||
api.getAll([
|
||||
'migrations?pending',
|
||||
'migrations?done'
|
||||
]).then(([{ migrations: pending }, { migrations: done }]) => {
|
||||
this.done = done.reverse()
|
||||
pending.forEach(migration => {
|
||||
if (migration.disclaimer) {
|
||||
migration.disclaimer = migration.disclaimer.replace('\n', '<br>')
|
||||
this.$set(this.checked, migration.id, null)
|
||||
}
|
||||
})
|
||||
// FIXME change to pending
|
||||
this.pending = pending.reverse()
|
||||
formatMigrationsData ({ migrations: pending }, { migrations: done }) {
|
||||
this.done = done.length ? done.reverse() : null
|
||||
pending.forEach(migration => {
|
||||
if (migration.disclaimer) {
|
||||
migration.disclaimer = migration.disclaimer.replace('\n', '<br>')
|
||||
this.$set(this.checked, migration.id, null)
|
||||
}
|
||||
})
|
||||
// FIXME change to pending
|
||||
this.pending = pending.length ? pending.reverse() : null
|
||||
},
|
||||
|
||||
runMigrations () {
|
||||
|
@ -147,17 +121,20 @@ export default {
|
|||
}
|
||||
// Check that every migration's disclaimer has been checked.
|
||||
if (Object.values(this.checked).every(value => value === true)) {
|
||||
api.post('migrations/migrate', { accept_disclaimer: true }).then(this.fetchData)
|
||||
api.post('migrations/migrate', { accept_disclaimer: true }).then(() => {
|
||||
this.$refs.view.fetchQueries()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
skipMigration () {
|
||||
api.post('/migrations/migrate', { skip: true, targets: this.skipId }).then(this.fetchData)
|
||||
}
|
||||
},
|
||||
async skipMigration (id) {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_migrations_skip'))
|
||||
if (!confirmed) return
|
||||
|
||||
created () {
|
||||
this.fetchData()
|
||||
api.post('/migrations/migrate', { skip: true, targets: id }).then(() => {
|
||||
this.$refs.view.fetchQueries()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
<template>
|
||||
<div class="tool-power">
|
||||
<div>
|
||||
<div v-if="inProcess">
|
||||
<b-alert variant="info" show v-t="'tools_' + action + '_done'" />
|
||||
<b-alert variant="info" v-t="'tools_' + action + '_done'" />
|
||||
|
||||
<b-alert variant="warning" show>
|
||||
<icon :iname="action === 'reboot' ? 'refresh' : 'power-off'" /> {{ $t(action === 'reboot' ? 'tools_rebooting' : 'tools_shuttingdown') }}
|
||||
<b-alert variant="warning">
|
||||
<icon :iname="action === 'reboot' ? 'refresh' : 'power-off'" />
|
||||
{{ $t(action === 'reboot' ? 'tools_rebooting' : 'tools_shuttingdown') }}
|
||||
</b-alert>
|
||||
<template v-if="canReconnect">
|
||||
<b-alert variant="success" show v-t="'tools_power_up'" />
|
||||
<b-alert variant="success" v-t="'tools_power_up'" />
|
||||
<login-view />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<b-card v-else>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="wrench" /> {{ $t('operations') }}</h2>
|
||||
</template>
|
||||
|
||||
<card v-else :title="$t('operations')" icon="wrench">
|
||||
<!-- REBOOT -->
|
||||
<b-form-group
|
||||
label-cols="5" label-cols-md="4" label-cols-lg="3"
|
||||
:label="$t('tools_reboot')" label-for="reboot"
|
||||
>
|
||||
<b-button
|
||||
variant="danger" id="reboot" v-b-modal.confirm-action
|
||||
@click="action = 'reboot'"
|
||||
>
|
||||
<b-button @click="triggerAction('reboot')" variant="danger" id="reboot">
|
||||
<icon iname="refresh" /> {{ $t('tools_reboot_btn') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
|
@ -36,23 +30,11 @@
|
|||
label-cols="5" label-cols-md="4" label-cols-lg="3"
|
||||
:label="$t('tools_shutdown')" label-for="shutdown"
|
||||
>
|
||||
<b-button
|
||||
variant="danger" id="shutdown" v-b-modal.confirm-action
|
||||
@click="action = 'shutdown'"
|
||||
>
|
||||
<b-button @click="triggerAction('shutdown')" variant="danger" id="shutdown">
|
||||
<icon iname="power-off" /> {{ $t('tools_shutdown_btn') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
|
||||
<!-- REBOOT/SHUTDOWN CONFIRM MODAL -->
|
||||
<b-modal
|
||||
centered hide-header
|
||||
id="confirm-action" body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="triggerAction(action)"
|
||||
>
|
||||
{{ $t('confirm_reboot_action_' + action) }}
|
||||
</b-modal>
|
||||
</b-card>
|
||||
</card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -72,7 +54,13 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
triggerAction (action) {
|
||||
async triggerAction (action) {
|
||||
const confirmed = await this.$askConfirmation(
|
||||
this.$i18n.t('confirm_reboot_action_' + action)
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
this.action = action
|
||||
api.put(action + '?force').then(() => {
|
||||
// Use 'RESET_CONNECTED' and not 'DISCONNECT' else user will be redirect to login
|
||||
this.$store.dispatch('RESET_CONNECTED')
|
||||
|
@ -100,8 +88,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
LoginView
|
||||
}
|
||||
components: { LoginView }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
<template>
|
||||
<card-form
|
||||
:title="$t('tools_webadmin_settings')" icon="cog"
|
||||
no-footer
|
||||
>
|
||||
<card-form :title="$t('tools_webadmin_settings')" icon="cog" no-footer>
|
||||
<template v-for="(field, fname) in fields">
|
||||
<form-field
|
||||
v-bind="field" v-model="self[fname]" :key="fname"
|
||||
/>
|
||||
<form-field v-bind="field" v-model="self[fname]" :key="fname" />
|
||||
<hr :key="fname + 'hr'">
|
||||
</template>
|
||||
</card-form>
|
||||
|
|
|
@ -1,30 +1,18 @@
|
|||
<template>
|
||||
<div class="system-update">
|
||||
<!-- FIXME add perform update button ? -->
|
||||
<!-- <div class="actions">
|
||||
<div class="buttons ml-auto">
|
||||
<b-button variant="success" @click="performUpdate">
|
||||
<icon iname="refresh" /> {{ $t('system_update') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<view-base :loading="loading" skeleton="card-list-skeleton">
|
||||
<!-- MIGRATIONS WARN -->
|
||||
<b-alert variant="warning" :show="migrationsNotDone">
|
||||
<icon iname="exclamation-triangle" /> <span v-html="$t('pending_migrations')" />
|
||||
</b-alert>
|
||||
|
||||
<!-- SYSTEM UPGRADE -->
|
||||
<b-card no-body>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="server" /> {{ $t('system') }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="$t('system')" icon="server" no-body>
|
||||
<b-list-group v-if="system" flush>
|
||||
<b-list-group-item
|
||||
v-for="{ name, current_version, new_version } in system" :key="name"
|
||||
>
|
||||
<h5 class="m-0">{{ name }} <small>({{ $t('from_to', [current_version, new_version]) }})</small></h5>
|
||||
<b-list-group-item v-for="{ name, current_version, new_version } in system" :key="name">
|
||||
<h5 class="m-0">
|
||||
{{ name }}
|
||||
<small>({{ $t('from_to', [current_version, new_version]) }})</small>
|
||||
</h5>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
||||
|
@ -32,34 +20,29 @@
|
|||
<span class="text-success"><icon iname="check-circle" /> {{ $t('system_packages_nothing') }}</span>
|
||||
</b-card-body>
|
||||
|
||||
<template v-if="system" v-slot:footer>
|
||||
<div class="d-flex justify-content-end">
|
||||
<b-button
|
||||
v-b-modal.confirm-upgrade variant="success"
|
||||
v-t="'system_upgrade_all_packages_btn'"
|
||||
@click="action = ['system']"
|
||||
/>
|
||||
</div>
|
||||
<template #buttons v-if="system">
|
||||
<b-button
|
||||
variant="success" v-t="'system_upgrade_all_packages_btn'"
|
||||
@click="performUpgrade({ type: 'system' })"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<!-- APPS UPGRADE -->
|
||||
<b-card no-body>
|
||||
<template v-slot:header>
|
||||
<h2><icon iname="cubes" /> {{ $t('applications') }}</h2>
|
||||
</template>
|
||||
|
||||
<card :title="$t('applications')" icon="cubes" no-body>
|
||||
<b-list-group v-if="apps" flush>
|
||||
<b-list-group-item
|
||||
v-for="{ label, id, current_version, new_version } in apps" :key="id"
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h5 class="m-0">{{ label }} <small>({{ id }}) {{ $t('from_to', [current_version, new_version]) }}</small></h5>
|
||||
<h5 class="m-0">
|
||||
{{ label }}
|
||||
<small>({{ id }}) {{ $t('from_to', [current_version, new_version]) }}</small>
|
||||
</h5>
|
||||
|
||||
<b-button
|
||||
v-b-modal.confirm-upgrade variant="success" size="sm"
|
||||
v-t="'system_upgrade_btn'"
|
||||
@click="action = ['specific_app', id]"
|
||||
variant="success" size="sm" v-t="'system_upgrade_btn'"
|
||||
@click="performUpgrade({ type: 'specific_app', id })"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -68,27 +51,14 @@
|
|||
<span class="text-success"><icon iname="check-circle" /> {{ $t('system_apps_nothing') }}</span>
|
||||
</b-card-body>
|
||||
|
||||
<template v-if="apps" v-slot:footer>
|
||||
<div class="d-flex justify-content-end">
|
||||
<b-button
|
||||
v-b-modal.confirm-upgrade variant="success"
|
||||
v-t="'system_upgrade_all_applications_btn'"
|
||||
@click="action = ['apps']"
|
||||
/>
|
||||
</div>
|
||||
<template #buttons v-if="apps">
|
||||
<b-button
|
||||
variant="success" v-t="'system_upgrade_all_applications_btn'"
|
||||
@click="performUpgrade({ type: 'apps' })"
|
||||
/>
|
||||
</template>
|
||||
</b-card>
|
||||
|
||||
<!-- UPGRADE CONFIRM MODAL -->
|
||||
<b-modal
|
||||
v-if="action"
|
||||
id="confirm-upgrade" centered
|
||||
body-bg-variant="danger" body-text-variant="light"
|
||||
@ok="performUpgrade" hide-header
|
||||
>
|
||||
{{ $t('confirm_update_' + action[0], action[1] ? { app: action[1] } : {}) }}
|
||||
</b-modal>
|
||||
</div>
|
||||
</card>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -99,9 +69,8 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
action: undefined,
|
||||
app: undefined,
|
||||
// api data
|
||||
loading: true,
|
||||
// API data
|
||||
migrationsNotDone: undefined,
|
||||
system: undefined,
|
||||
apps: undefined
|
||||
|
@ -109,21 +78,11 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
async fetchData () {
|
||||
api.get('migrations?pending').then(({ migrations }) => {
|
||||
this.migrationsNotDone = migrations.length !== 0
|
||||
})
|
||||
},
|
||||
async performUpgrade ({ type, id = null }) {
|
||||
const confirmMsg = this.$i18n.t('confirm_update_' + type, id ? { app: id } : {})
|
||||
const confirmed = await this.$askConfirmation(confirmMsg)
|
||||
if (!confirmed) return
|
||||
|
||||
performUpdate () {
|
||||
api.put('update').then(({ apps, system }) => {
|
||||
this.apps = apps.length ? apps : null
|
||||
this.system = system.length ? system : null
|
||||
})
|
||||
},
|
||||
|
||||
performUpgrade () {
|
||||
const [type, id] = this.action
|
||||
const uri = type === 'specific_app'
|
||||
? 'upgrade/apps?app=' + id
|
||||
: 'upgrade?' + type
|
||||
|
@ -135,9 +94,17 @@ export default {
|
|||
},
|
||||
|
||||
created () {
|
||||
// FIXME Do not perform directly the update ?
|
||||
this.performUpdate()
|
||||
this.fetchData()
|
||||
// Since we need to query a `PUT` method, we won't use ViewBase's `queries` prop and
|
||||
// its automatic loading handling.
|
||||
Promise.all([
|
||||
api.get('migrations?pending'),
|
||||
api.put('update')
|
||||
]).then(([{ migrations }, { apps, system }]) => {
|
||||
this.migrationsNotDone = migrations.length !== 0
|
||||
this.apps = apps.length ? apps : null
|
||||
this.system = system.length ? system : null
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,60 +1,62 @@
|
|||
<template>
|
||||
<card-form
|
||||
:title="$t('users_new')" icon="user-plus"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<!-- USER NAME -->
|
||||
<form-field v-bind="fields.username" v-model="form.username" :validation="$v.form.username" />
|
||||
|
||||
<!-- USER FULLNAME -->
|
||||
<form-field
|
||||
v-bind="fields.fullname" :validation="$v.form.fullname"
|
||||
<view-base :queries="queries" @queries-response="onQueriesResponse" skeleton="card-form-skeleton">
|
||||
<card-form
|
||||
:title="$t('users_new')" icon="user-plus"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<template v-for="fname in ['firstname', 'lastname']">
|
||||
<b-input-group-prepend :key="fname + 'prepend'">
|
||||
<b-input-group-text :id="fname + '-label'" tag="label">
|
||||
{{ self[fname].label }}
|
||||
<!-- USER NAME -->
|
||||
<form-field v-bind="fields.username" v-model="form.username" :validation="$v.form.username" />
|
||||
|
||||
<!-- USER FULLNAME -->
|
||||
<form-field
|
||||
v-bind="fields.fullname" :validation="$v.form.fullname"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<template v-for="fname in ['firstname', 'lastname']">
|
||||
<b-input-group-prepend :key="fname + 'prepend'">
|
||||
<b-input-group-text :id="fname + '-label'" tag="label">
|
||||
{{ self[fname].label }}
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<input-item
|
||||
v-bind="self[fname]" v-model="form.fullname[fname]" :key="fname + 'input'"
|
||||
:name="self[fname].id" :aria-labelledby="fname + '-label'"
|
||||
/>
|
||||
</template>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<!-- USER MAIL DOMAIN -->
|
||||
<form-field v-bind="fields.domain" :validation="$v.form.domain">
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text id="local-part" tag="label" class="border-right-0">
|
||||
{{ form.username }}@
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
</b-input-group-append>
|
||||
|
||||
<input-item
|
||||
v-bind="self[fname]" v-model="form.fullname[fname]" :key="fname + 'input'"
|
||||
:name="self[fname].id" :aria-labelledby="fname + '-label'"
|
||||
<select-item
|
||||
aria-labelledby="local-part" aria-describedby="mail__BV_description_"
|
||||
v-model="form.domain" v-bind="self"
|
||||
/>
|
||||
</template>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<!-- USER MAIL DOMAIN -->
|
||||
<form-field v-bind="fields.domain" :validation="$v.form.domain">
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text id="local-part" tag="label" class="border-right-0">
|
||||
{{ form.username }}@
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
<!-- USER PASSWORD -->
|
||||
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
||||
|
||||
<select-item
|
||||
aria-labelledby="local-part" aria-describedby="mail__BV_description_"
|
||||
v-model="form.domain" v-bind="self"
|
||||
/>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<!-- USER PASSWORD -->
|
||||
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
||||
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<form-field v-bind="fields.confirmation" v-model="form.confirmation" :validation="$v.form.confirmation" />
|
||||
</card-form>
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<form-field v-bind="fields.confirmation" v-model="form.confirmation" :validation="$v.form.confirmation" />
|
||||
</card-form>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -66,14 +68,17 @@ import {
|
|||
alphalownum_, unique, required, minLength, name, sameAs
|
||||
} from '@/helpers/validators'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'UserCreate',
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
{ uri: 'users' },
|
||||
{ uri: 'domains' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' }
|
||||
],
|
||||
|
||||
form: {
|
||||
username: '',
|
||||
fullname: {
|
||||
|
@ -162,6 +167,11 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onQueriesResponse () {
|
||||
this.fields.domain.props.choices = this.domainsAsChoices
|
||||
this.form.domain = this.mainDomain
|
||||
},
|
||||
|
||||
onSubmit () {
|
||||
const data = formatFormData(this.form, { flatten: true })
|
||||
this.$store.dispatch(
|
||||
|
@ -174,14 +184,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('FETCH_ALL',
|
||||
[{ uri: 'domains' }, { uri: 'users' }, { uri: 'domains/main', storeKey: 'main_domain' }]
|
||||
).then(([domains]) => {
|
||||
this.fields.domain.props.choices = this.domainsAsChoices
|
||||
this.form.domain = this.mainDomain
|
||||
})
|
||||
}
|
||||
mixins: [validationMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,108 +1,110 @@
|
|||
<template lang="html">
|
||||
<card-form
|
||||
:title="$t('user_username_edit', { name })" icon="user"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<!-- USERNAME (disabled) -->
|
||||
<form-field v-bind="fields.username" />
|
||||
<template>
|
||||
<view-base :queries="queries" @queries-response="onQueriesResponse" skeleton="card-form-skeleton">
|
||||
<card-form
|
||||
:title="$t('user_username_edit', { name })" icon="user"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<!-- USERNAME (disabled) -->
|
||||
<form-field v-bind="fields.username" />
|
||||
|
||||
<!-- USER FULLNAME (FIXME quite a mess, but will be removed)-->
|
||||
<form-field v-bind="fields.fullname" :validation="$v.form.fullname">
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<template v-for="name_ in ['firstname', 'lastname']">
|
||||
<b-input-group-prepend :key="name_ + 'prepend'">
|
||||
<b-input-group-text :id="name_ + '-label'" tag="label">
|
||||
{{ self[name_].label }}
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
<!-- USER FULLNAME (FIXME quite a mess, but will be removed)-->
|
||||
<form-field v-bind="fields.fullname" :validation="$v.form.fullname">
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<template v-for="name_ in ['firstname', 'lastname']">
|
||||
<b-input-group-prepend :key="name_ + 'prepend'">
|
||||
<b-input-group-text :id="name_ + '-label'" tag="label">
|
||||
{{ self[name_].label }}
|
||||
</b-input-group-text>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<input-item
|
||||
v-bind="self[name_]" v-model.trim="form.fullname[name_]" :key="name_ + 'input'"
|
||||
:name="self[name_].id" :aria-labelledby="name_ + '-label'"
|
||||
:state="$v.form.fullname[name_].$invalid && $v.form.fullname.$anyDirty ? false : null"
|
||||
/>
|
||||
</template>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
<input-item
|
||||
v-bind="self[name_]" v-model.trim="form.fullname[name_]" :key="name_ + 'input'"
|
||||
:name="self[name_].id" :aria-labelledby="name_ + '-label'"
|
||||
:state="$v.form.fullname[name_].$invalid && $v.form.fullname.$anyDirty ? false : null"
|
||||
/>
|
||||
</template>
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<!-- USER EMAIL -->
|
||||
<form-field v-bind="fields.mail" :validation="$v.form.mail">
|
||||
<template #default="{ self }">
|
||||
<adress-input-select v-bind="self" v-model="form.mail" />
|
||||
</template>
|
||||
</form-field>
|
||||
<!-- USER EMAIL -->
|
||||
<form-field v-bind="fields.mail" :validation="$v.form.mail">
|
||||
<template #default="{ self }">
|
||||
<adress-input-select v-bind="self" v-model="form.mail" />
|
||||
</template>
|
||||
</form-field>
|
||||
|
||||
<!-- MAILBOX QUOTA -->
|
||||
<form-field v-bind="fields.mailbox_quota" :validation="$v.form.mailbox_quota">
|
||||
<template #default="{ self }">
|
||||
<b-input-group append="M">
|
||||
<input-item v-bind="self" v-model="form.mailbox_quota" />
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
<!-- MAILBOX QUOTA -->
|
||||
<form-field v-bind="fields.mailbox_quota" :validation="$v.form.mailbox_quota">
|
||||
<template #default="{ self }">
|
||||
<b-input-group append="M">
|
||||
<input-item v-bind="self" v-model="form.mailbox_quota" />
|
||||
</b-input-group>
|
||||
</template>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<!-- MAIL ALIASES -->
|
||||
<form-field :label="$t('user_emailaliases')" id="mail-aliases">
|
||||
<div
|
||||
v-for="(mail, i) in form.mail_aliases" :key="i"
|
||||
class="mail-list"
|
||||
>
|
||||
<form-field
|
||||
v-bind="fields.mail_aliases"
|
||||
:id="'mail_aliases' + i"
|
||||
:validation="$v.form.mail_aliases.$each[i]"
|
||||
<!-- MAIL ALIASES -->
|
||||
<form-field :label="$t('user_emailaliases')" id="mail-aliases">
|
||||
<div
|
||||
v-for="(mail, i) in form.mail_aliases" :key="i"
|
||||
class="mail-list"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<adress-input-select v-bind="self" v-model="form.mail_aliases[i]" />
|
||||
</template>
|
||||
</form-field>
|
||||
<form-field
|
||||
v-bind="fields.mail_aliases"
|
||||
:id="'mail_aliases' + i"
|
||||
:validation="$v.form.mail_aliases.$each[i]"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<adress-input-select v-bind="self" v-model="form.mail_aliases[i]" />
|
||||
</template>
|
||||
</form-field>
|
||||
|
||||
<b-button variant="danger" @click="removeEmailField('aliases', i)">
|
||||
<icon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="sr-only">{{ $t('delete') }}</span>
|
||||
<b-button variant="danger" @click="removeEmailField('aliases', i)">
|
||||
<icon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="sr-only">{{ $t('delete') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-button variant="success" @click="addEmailField('aliases')">
|
||||
<icon iname="plus" /> {{ $t('user_emailaliases_add') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form-field>
|
||||
|
||||
<b-button variant="success" @click="addEmailField('aliases')">
|
||||
<icon iname="plus" /> {{ $t('user_emailaliases_add') }}
|
||||
</b-button>
|
||||
</form-field>
|
||||
<!-- MAIL FORWARD -->
|
||||
<form-field :label="$t('user_emailforward')" id="mail-forward">
|
||||
<div
|
||||
v-for="(mail, i) in form.mail_forward" :key="i"
|
||||
class="mail-list"
|
||||
>
|
||||
<form-field
|
||||
v-bind="fields.mail_forward" v-model="form.mail_forward[i]"
|
||||
:id="'mail-forward' + i"
|
||||
:validation="$v.form.mail_forward.$each[i]"
|
||||
/>
|
||||
|
||||
<!-- MAIL FORWARD -->
|
||||
<form-field :label="$t('user_emailforward')" id="mail-forward">
|
||||
<div
|
||||
v-for="(mail, i) in form.mail_forward" :key="i"
|
||||
class="mail-list"
|
||||
>
|
||||
<form-field
|
||||
v-bind="fields.mail_forward" v-model="form.mail_forward[i]"
|
||||
:id="'mail-forward' + i"
|
||||
:validation="$v.form.mail_forward.$each[i]"
|
||||
/>
|
||||
<b-button variant="danger" @click="removeEmailField('forward', i)">
|
||||
<icon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="sr-only">{{ $t('delete') }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<b-button variant="danger" @click="removeEmailField('forward', i)">
|
||||
<icon :title="$t('delete')" iname="trash-o" />
|
||||
<span class="sr-only">{{ $t('delete') }}</span>
|
||||
<b-button variant="success" @click="addEmailField('forward')">
|
||||
<icon iname="plus" /> {{ $t('user_emailforward_add') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</form-field>
|
||||
<hr>
|
||||
|
||||
<b-button variant="success" @click="addEmailField('forward')">
|
||||
<icon iname="plus" /> {{ $t('user_emailforward_add') }}
|
||||
</b-button>
|
||||
</form-field>
|
||||
<hr>
|
||||
<!-- USER PASSWORD -->
|
||||
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
||||
|
||||
<!-- USER PASSWORD -->
|
||||
<form-field v-bind="fields.password" v-model="form.password" :validation="$v.form.password" />
|
||||
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<form-field v-bind="fields.confirmation" v-model="form.confirmation" :validation="$v.form.confirmation" />
|
||||
</card-form>
|
||||
<!-- USER PASSWORD CONFIRMATION -->
|
||||
<form-field v-bind="fields.confirmation" v-model="form.confirmation" :validation="$v.form.confirmation" />
|
||||
</card-form>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -127,7 +129,11 @@ export default {
|
|||
|
||||
data () {
|
||||
return {
|
||||
ready: false,
|
||||
queries: [
|
||||
{ uri: 'users', param: this.name, storeKey: 'users_details' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'domains' }
|
||||
],
|
||||
|
||||
form: {
|
||||
fullname: { firstname: '', lastname: '' },
|
||||
|
@ -236,6 +242,27 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onQueriesResponse (user) {
|
||||
this.fields.mail.props.choices = this.domainsAsChoices
|
||||
this.fields.mail_aliases.props.choices = this.domainsAsChoices
|
||||
|
||||
this.form.fullname = {
|
||||
// Copy value to avoid refering to the stored user data
|
||||
firstname: user.firstname.valueOf(),
|
||||
lastname: user.lastname.valueOf()
|
||||
}
|
||||
this.form.mail = adressToFormValue(user.mail)
|
||||
if (user['mail-aliases']) {
|
||||
this.form.mail_aliases = user['mail-aliases'].map(mail => adressToFormValue(mail))
|
||||
}
|
||||
if (user['mail-forward']) {
|
||||
this.form.mail_forward = user['mail-forward'].slice() // Copy value
|
||||
}
|
||||
if (user['mailbox-quota'].limit !== 'No quota') {
|
||||
this.form.mailbox_quota = sizeToM(user['mailbox-quota'].limit)
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit () {
|
||||
const formData = formatFormData(this.form, { flatten: true })
|
||||
const user = this.user(this.name)
|
||||
|
@ -280,8 +307,9 @@ export default {
|
|||
? { localPart: '', separator: '@', domain: this.mainDomain }
|
||||
: ''
|
||||
)
|
||||
// Focus last input after rendering update
|
||||
this.$nextTick(() => {
|
||||
const inputs = document.querySelectorAll(`#mail-${type} input`)
|
||||
const inputs = this.$el.querySelectorAll(`#mail-${type} input`)
|
||||
inputs[inputs.length - 1].focus()
|
||||
})
|
||||
},
|
||||
|
@ -291,39 +319,8 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('FETCH_ALL', [
|
||||
{ uri: 'users', param: this.name, storeKey: 'users_details' },
|
||||
{ uri: 'domains/main', storeKey: 'main_domain' },
|
||||
{ uri: 'domains' }
|
||||
]).then(([user, mainDomain, domains]) => {
|
||||
this.fields.mail.props.choices = this.domainsAsChoices
|
||||
this.fields.mail_aliases.props.choices = this.domainsAsChoices
|
||||
|
||||
this.form.fullname = {
|
||||
// Copy value to avoid refering to the stored user data
|
||||
firstname: user.firstname.valueOf(),
|
||||
lastname: user.lastname.valueOf()
|
||||
}
|
||||
this.form.mail = adressToFormValue(user.mail)
|
||||
if (user['mail-aliases']) {
|
||||
this.form.mail_aliases = user['mail-aliases'].map(mail => adressToFormValue(mail))
|
||||
}
|
||||
if (user['mail-forward']) {
|
||||
this.form.mail_forward = user['mail-forward'].slice() // Copy value
|
||||
}
|
||||
if (user['mailbox-quota'].limit !== 'No quota') {
|
||||
this.form.mailbox_quota = sizeToM(user['mailbox-quota'].limit)
|
||||
}
|
||||
this.ready = true
|
||||
})
|
||||
},
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
||||
components: {
|
||||
AdressInputSelect
|
||||
}
|
||||
components: { AdressInputSelect }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,123 +1,105 @@
|
|||
<template>
|
||||
<div class="user">
|
||||
<b-card :class="{skeleton: !user}">
|
||||
<template v-slot:header>
|
||||
<h2>{{ user ? user.fullname : '' }}</h2>
|
||||
</template>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<view-base :queries="queries" skeleton="card-info-skeleton">
|
||||
<card v-if="user" :title="user.fullname" icon="user">
|
||||
<div class="d-flex align-items-center flex-column flex-md-row">
|
||||
<icon iname="user" class="fa-fw" />
|
||||
|
||||
<div class="w-100">
|
||||
<template v-if="user">
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_username') }}</strong></b-col>
|
||||
<b-col>{{ user.username }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_username') }}</strong></b-col>
|
||||
<b-col>{{ user.username }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_email') }}</strong></b-col>
|
||||
<b-col class="font-italic">
|
||||
{{ user.mail }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_email') }}</strong></b-col>
|
||||
<b-col class="font-italic">
|
||||
{{ user.mail }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].limit }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].limit }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].use }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].use }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType">
|
||||
<b-col><strong>{{ $t(trad) }}</strong></b-col>
|
||||
<b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType">
|
||||
<b-col><strong>{{ $t(trad) }}</strong></b-col>
|
||||
|
||||
<b-col v-if="user[mailType]">
|
||||
<ul v-if="user[mailType].length > 1">
|
||||
<li v-for="(alias, index) in user[mailType]" :key="index">
|
||||
{{ alias }}
|
||||
</li>
|
||||
</ul>
|
||||
<b-col v-if="user[mailType]">
|
||||
<ul v-if="user[mailType].length > 1">
|
||||
<li v-for="(alias, index) in user[mailType]" :key="index">
|
||||
{{ alias }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template v-else-if="user[mailType][0]">
|
||||
{{ user[mailType][0] }}
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<!-- skeleton -->
|
||||
<template v-else>
|
||||
<b-row v-for="(n, index) in 6" :key="index">
|
||||
<b-col>
|
||||
<strong class="rounded" />
|
||||
</b-col>
|
||||
|
||||
<b-col>
|
||||
<span v-if="n <= 4" class="rounded" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<template v-else-if="user[mailType][0]">
|
||||
{{ user[mailType][0] }}
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
<template v-slot:footer>
|
||||
<div class="d-flex d-flex justify-content-end">
|
||||
<b-button :to="user ? {name: 'user-edit', params: {user: user}} : null"
|
||||
:variant="user ? 'info' : 'dark'"
|
||||
>
|
||||
{{ user ? $t('user_username_edit', {name: user.username}) : '' }}
|
||||
</b-button>
|
||||
|
||||
<b-button :variant="user ? 'danger' : 'dark'" class="ml-2" v-b-modal.delete-modal>
|
||||
{{ user ? $t('delete') : '' }}
|
||||
</b-button>
|
||||
</div>
|
||||
<template #buttons>
|
||||
<b-button :to="{ name: 'user-edit', params: { user } }" :variant="user ? 'info' : 'dark'">
|
||||
<icon iname="edit" />
|
||||
{{ user ? $t('user_username_edit', {name: user.username}) : '' }}
|
||||
</b-button>
|
||||
|
||||
<b-button v-b-modal.delete-modal :variant="user ? 'danger' : 'dark'">
|
||||
<icon iname="trash-o" />
|
||||
{{ user ? $t('delete') : '' }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-card>
|
||||
</card>
|
||||
|
||||
<b-modal
|
||||
v-if="user" id="delete-modal" centered
|
||||
header-bg-variant="danger" header-text-variant="light"
|
||||
:title="$t('confirm_delete', {name: user.username })"
|
||||
@ok="deleteUser"
|
||||
v-if="user"
|
||||
id="delete-modal" :title="$t('confirm_delete', { name: user.username })" @ok="deleteUser"
|
||||
header-bg-variant="warning" body-class="" body-bg-variant=""
|
||||
>
|
||||
<b-form-group>
|
||||
<template v-slot:description>
|
||||
<b-alert variant="warning" show>
|
||||
<icon iname="exclamation-triangle" /> {{ $t('purge_user_data_warning') }}
|
||||
</b-alert>
|
||||
</template>
|
||||
<b-form-checkbox v-model="purge" class="mb-3">
|
||||
{{ $t('purge_user_data_checkbox', {name: user.username}) }}
|
||||
<b-form-checkbox v-model="purge">
|
||||
{{ $t('purge_user_data_checkbox', { name: user.username }) }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<template #description>
|
||||
<div class="alert alert-warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('purge_user_data_warning') }}
|
||||
</div>
|
||||
</template>
|
||||
</b-form-group>
|
||||
</b-modal>
|
||||
</div>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'UserInfo',
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
name: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [{ uri: 'users', param: this.name, storeKey: 'users_details' }],
|
||||
purge: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.data.users_details[this.name]
|
||||
return this.$store.getters.user(this.name)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
deleteUser () {
|
||||
const data = this.purge ? { purge: '' } : {}
|
||||
|
@ -127,23 +109,11 @@ export default {
|
|||
this.$router.push({ name: 'user-list' })
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('FETCH',
|
||||
{ uri: 'users', param: this.name, storeKey: 'users_details' }
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-body > div {
|
||||
flex-direction: column;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.icon.fa-user {
|
||||
font-size: 10rem;
|
||||
padding-right: 3rem;
|
||||
|
@ -171,30 +141,4 @@ ul {
|
|||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
opacity: 0.5;
|
||||
|
||||
h2 {
|
||||
height: #{2 * 1.2}rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
& > * {
|
||||
display: block;
|
||||
background-color: $skeleton-color;
|
||||
height: 1.5rem;
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
max-width: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
height: calc(2.25rem + 2px);
|
||||
width: 7rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<search-view
|
||||
id="user-list"
|
||||
<view-search
|
||||
:search.sync="search"
|
||||
:items="users"
|
||||
:filtered-items="filteredUsers"
|
||||
items-name="users"
|
||||
:queries="queries"
|
||||
>
|
||||
<template #top-bar-buttons>
|
||||
<b-button variant="info" :to="{ name: 'group-list' }">
|
||||
|
@ -36,19 +36,18 @@
|
|||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</search-view>
|
||||
</view-search>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import SearchView from '@/components/SearchView'
|
||||
|
||||
export default {
|
||||
name: 'UserList',
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [{ uri: 'users' }],
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
|
@ -64,12 +63,6 @@ export default {
|
|||
})
|
||||
return filtered.length === 0 ? null : filtered
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$store.dispatch('FETCH', { uri: 'users' })
|
||||
},
|
||||
|
||||
components: { SearchView }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue