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