mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
linting…
This commit is contained in:
parent
d69bb8b906
commit
6f8457381a
15 changed files with 577 additions and 546 deletions
249
app/src/App.vue
249
app/src/App.vue
|
@ -1,48 +1,64 @@
|
|||
<template>
|
||||
<div id="app" class="container">
|
||||
<header>
|
||||
<b-navbar>
|
||||
<b-navbar-brand to="/" exact exact-active-class="active"><img alt="Yunohost logo" src="./assets/logo.png"></b-navbar-brand>
|
||||
<b-navbar-nav class="ml-auto">
|
||||
<li class="nav-item">
|
||||
<b-button href="/yunohost/sso" variant="primary" block size="sm">
|
||||
{{ $t('user_interface_link') }} <icon iname="user"/>
|
||||
</b-button>
|
||||
</li>
|
||||
<li class="nav-item" v-show="connected">
|
||||
<b-button @click.prevent="logout" to="/logout" variant="outline-dark" block size="sm" >
|
||||
{{ $t('logout') }} <icon iname="sign-out"/>
|
||||
</b-button>
|
||||
</li>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
</header>
|
||||
<div id="app" class="container">
|
||||
<header>
|
||||
<b-navbar>
|
||||
<b-navbar-brand to="/" exact exact-active-class="active">
|
||||
<img alt="Yunohost logo" src="./assets/logo.png">
|
||||
</b-navbar-brand>
|
||||
<b-navbar-nav class="ml-auto">
|
||||
<li class="nav-item">
|
||||
<b-button href="/yunohost/sso"
|
||||
variant="primary" size="sm" block
|
||||
>
|
||||
{{ $t('user_interface_link') }} <icon iname="user" />
|
||||
</b-button>
|
||||
</li>
|
||||
<li class="nav-item" v-show="connected">
|
||||
<b-button @click.prevent="logout" to="/logout"
|
||||
variant="outline-dark" block size="sm"
|
||||
>
|
||||
{{ $t('logout') }} <icon iname="sign-out" />
|
||||
</b-button>
|
||||
</li>
|
||||
</b-navbar-nav>
|
||||
</b-navbar>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<router-view v-if="isReady"/>
|
||||
</main>
|
||||
<main>
|
||||
<router-view v-if="isReady" />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<nav>
|
||||
<b-nav>
|
||||
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes='text-secondary'>
|
||||
<icon iname="book"/> Documentation
|
||||
</b-nav-item>
|
||||
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes='text-secondary'>
|
||||
<icon iname="life-ring"/> Need help?
|
||||
</b-nav-item>
|
||||
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes='text-secondary'>
|
||||
<icon iname="heart"/> Donate
|
||||
</b-nav-item>
|
||||
<i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text" class="ml-auto" id="yunohost-version">
|
||||
<template v-slot:ynh><b-link href="https://yunohost.org">YunoHost</b-link></template>
|
||||
<template v-slot:version>{{ yunohostInfos.version }}</template>
|
||||
<template v-slot:repo>{{ yunohostInfos.repo }}</template>
|
||||
</i18n>
|
||||
</b-nav>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
<footer>
|
||||
<nav>
|
||||
<b-nav>
|
||||
<b-nav-item href="https://yunohost.org/docs" target="_blank" link-classes="text-secondary">
|
||||
<icon iname="book" /> Documentation
|
||||
</b-nav-item>
|
||||
<b-nav-item href="https://yunohost.org/help" target="_blank" link-classes="text-secondary">
|
||||
<icon iname="life-ring" /> Need help?
|
||||
</b-nav-item>
|
||||
<b-nav-item href="https://donate.yunohost.org/" target="_blank" link-classes="text-secondary">
|
||||
<icon iname="heart" /> Donate
|
||||
</b-nav-item>
|
||||
<i18n v-if="yunohostInfos" path="footer_version" tag="b-nav-text"
|
||||
id="yunohost-version" class="ml-auto"
|
||||
>
|
||||
<template v-slot:ynh>
|
||||
<b-link href="https://yunohost.org">
|
||||
YunoHost
|
||||
</b-link>
|
||||
</template>
|
||||
<template v-slot:version>
|
||||
{{ yunohostInfos.version }}
|
||||
</template>
|
||||
<template v-slot:repo>
|
||||
{{ yunohostInfos.repo }}
|
||||
</template>
|
||||
</i18n>
|
||||
</b-nav>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -50,50 +66,49 @@ import { mapState } from 'vuex'
|
|||
|
||||
import api from '@/helpers/api'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data: () => {
|
||||
return {
|
||||
// isReady blocks the rendering of the rooter-view until we have a true info
|
||||
// about the connected state of the user.
|
||||
isReady: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['connected', 'yunohostInfos']),
|
||||
},
|
||||
methods: {
|
||||
async logout() {
|
||||
await api.logout()
|
||||
this.$store.commit('CONNECTED', false);
|
||||
this.$router.push('/login')
|
||||
},
|
||||
},
|
||||
// This hook is only triggered at page reload so the value of state.connected
|
||||
// always come from the localStorage
|
||||
async created() {
|
||||
if (!this.$store.state.connected) {
|
||||
// user is not connected: allow the login view to be rendered.
|
||||
this.isReady = true
|
||||
return
|
||||
}
|
||||
// localStorage 'connected' value may be true, but session may have expired.
|
||||
// Try to get the yunohost version.
|
||||
try {
|
||||
const data = await api.getVersion()
|
||||
this.$store.commit('YUNOHOST_INFOS', data.yunohost)
|
||||
} catch (err) {
|
||||
// Session expired, reset the 'connected' state and redirect with a query
|
||||
// FIXME is there a case where the error may not be a 401 therefor requires
|
||||
// better handling ?
|
||||
this.$store.commit('CONNECTED', false);
|
||||
this.$router.push({name: 'login', query: {redirect: this.$route.path}})
|
||||
} finally {
|
||||
// in any case allow the router-view to be rendered
|
||||
this.isReady = true;
|
||||
}
|
||||
name: 'App',
|
||||
data: () => {
|
||||
return {
|
||||
// isReady blocks the rendering of the rooter-view until we have a true info
|
||||
// about the connected state of the user.
|
||||
isReady: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['connected', 'yunohostInfos'])
|
||||
},
|
||||
methods: {
|
||||
async logout () {
|
||||
await api.logout()
|
||||
this.$store.commit('CONNECTED', false)
|
||||
this.$router.push('/login')
|
||||
}
|
||||
},
|
||||
// This hook is only triggered at page reload so the value of state.connected
|
||||
// always come from the localStorage
|
||||
async created () {
|
||||
if (!this.$store.state.connected) {
|
||||
// user is not connected: allow the login view to be rendered.
|
||||
this.isReady = true
|
||||
return
|
||||
}
|
||||
// localStorage 'connected' value may be true, but session may have expired.
|
||||
// Try to get the yunohost version.
|
||||
try {
|
||||
const data = await api.getVersion()
|
||||
this.$store.commit('YUNOHOST_INFOS', data.yunohost)
|
||||
} catch (err) {
|
||||
// Session expired, reset the 'connected' state and redirect with a query
|
||||
// FIXME is there a case where the error may not be a 401 therefor requires
|
||||
// better handling ?
|
||||
this.$store.commit('CONNECTED', false)
|
||||
this.$router.push({ name: 'login', query: { redirect: this.$route.path } })
|
||||
} finally {
|
||||
// in any case allow the router-view to be rendered
|
||||
this.isReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -101,46 +116,46 @@ export default {
|
|||
@import '@/scss/main.scss';
|
||||
|
||||
#app > header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.navbar {
|
||||
padding: 1rem 0;
|
||||
.navbar {
|
||||
padding: 1rem 0;
|
||||
|
||||
img {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: column;
|
||||
|
||||
li {
|
||||
margin: .2rem 0;
|
||||
}
|
||||
icon {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: column;
|
||||
|
||||
li {
|
||||
margin: .2rem 0;
|
||||
}
|
||||
icon {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#app > footer {
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid #eee;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 2rem;
|
||||
|
||||
.nav-item {
|
||||
& + .nav-item a::before {
|
||||
content: "•";
|
||||
width: 1rem;
|
||||
display: inline-block;
|
||||
margin-left: -1.15rem;
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
.nav-item {
|
||||
& + .nav-item a::before {
|
||||
content: "•";
|
||||
width: 1rem;
|
||||
display: inline-block;
|
||||
margin-left: -1.15rem;
|
||||
}
|
||||
&:first-child {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,51 +1,51 @@
|
|||
<template>
|
||||
<b-breadcrumb>
|
||||
<b-breadcrumb-item to="/">
|
||||
<span class="sr-only">{{ $t('home') }}</span>
|
||||
<icon iname="home"/>
|
||||
</b-breadcrumb-item>
|
||||
<b-breadcrumb-item
|
||||
v-for="(route, index) in breadcrumb"
|
||||
:key="index"
|
||||
:to="{name: route.name}"
|
||||
:active="index == lastIndex ? true : false"
|
||||
>
|
||||
{{ route.text }}
|
||||
</b-breadcrumb-item>
|
||||
</b-breadcrumb>
|
||||
<b-breadcrumb>
|
||||
<b-breadcrumb-item to="/">
|
||||
<span class="sr-only">{{ $t('home') }}</span>
|
||||
<icon iname="home" />
|
||||
</b-breadcrumb-item>
|
||||
<b-breadcrumb-item
|
||||
v-for="(route, index) in breadcrumb"
|
||||
:key="index"
|
||||
:to="{name: route.name}"
|
||||
:active="index == lastIndex ? true : false"
|
||||
>
|
||||
{{ route.text }}
|
||||
</b-breadcrumb-item>
|
||||
</b-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
params: function () {
|
||||
return this.$route.params
|
||||
},
|
||||
breadcrumb: function () {
|
||||
return this.$route.meta.breadcrumb.map(({name, trad, param}) => {
|
||||
let text = ''
|
||||
// if a traduction key string has been given and we also need to pass
|
||||
// the route param as a variable.
|
||||
if (trad && param) {
|
||||
text = this.$i18n.t(trad, {[param]: this.params[param]})
|
||||
} else if (trad) {
|
||||
text = this.$i18n.t(trad)
|
||||
} else {
|
||||
text = this.params[param]
|
||||
}
|
||||
return {name, text}
|
||||
})
|
||||
},
|
||||
lastIndex: function () {
|
||||
return this.breadcrumb.length - 1
|
||||
},
|
||||
computed: {
|
||||
params: function () {
|
||||
return this.$route.params
|
||||
},
|
||||
breadcrumb: function () {
|
||||
return this.$route.meta.breadcrumb.map(({ name, trad, param }) => {
|
||||
let text = ''
|
||||
// if a traduction key string has been given and we also need to pass
|
||||
// the route param as a variable.
|
||||
if (trad && param) {
|
||||
text = this.$i18n.t(trad, { [param]: this.params[param] })
|
||||
} else if (trad) {
|
||||
text = this.$i18n.t(trad)
|
||||
} else {
|
||||
text = this.params[param]
|
||||
}
|
||||
return { name, text }
|
||||
})
|
||||
},
|
||||
lastIndex: function () {
|
||||
return this.breadcrumb.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.breadcrumb {
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
<template>
|
||||
<span :class="'icon fa fa-' + iname" aria-hidden="true"></span>
|
||||
<span :class="'icon fa fa-' + iname" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'icon',
|
||||
props: ['iname'],
|
||||
name: 'Icon',
|
||||
props: {
|
||||
iname: { type: String, required: true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
|
||||
&.lg {
|
||||
width: 3rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.fs-sm {
|
||||
font-size: 1rem;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
|
||||
&.lg {
|
||||
width: 3rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&.fs-sm {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1 +1 @@
|
|||
export {default as Icon} from './Icon'
|
||||
export { default as Icon } from './Icon'
|
||||
|
|
|
@ -1,63 +1,57 @@
|
|||
|
||||
|
||||
function objectToParams(object) {
|
||||
const urlParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
urlParams.append(key, value)
|
||||
}
|
||||
return urlParams
|
||||
function objectToParams (object) {
|
||||
const urlParams = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
urlParams.append(key, value)
|
||||
}
|
||||
return urlParams
|
||||
}
|
||||
|
||||
|
||||
function handleResponse(response, type = 'json') {
|
||||
return response.ok ? response[type]() : handleErrors(response)
|
||||
function handleResponse (response, type = 'json') {
|
||||
return response.ok ? response[type]() : handleErrors(response)
|
||||
}
|
||||
|
||||
|
||||
function handleErrors(response) {
|
||||
if (response.status == 401) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
function handleErrors (response) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
options: {
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
// FIXME is it important to keep this previous `Accept` header ?
|
||||
// 'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
// Auto header is :
|
||||
// "Accept": "*/*",
|
||||
options: {
|
||||
credentials: 'include',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
// FIXME is it important to keep this previous `Accept` header ?
|
||||
// 'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
// Auto header is :
|
||||
// "Accept": "*/*",
|
||||
|
||||
// Also is this still important ? (needed by back-end)
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
},
|
||||
|
||||
get(uri) {
|
||||
return fetch('/api/' + uri, this.options)
|
||||
.then(response => handleResponse(response))
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
|
||||
login(password) {
|
||||
return fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: objectToParams({password}),
|
||||
...this.options
|
||||
}).then(response => (response.ok))
|
||||
},
|
||||
|
||||
logout() {
|
||||
return fetch('/api/logout', this.options).then(response => (response.ok))
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
return fetch('/api/versions', this.options)
|
||||
.then(response => handleResponse(response))
|
||||
// Also is this still important ? (needed by back-end)
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
},
|
||||
|
||||
get (uri) {
|
||||
return fetch('/api/' + uri, this.options)
|
||||
.then(response => handleResponse(response))
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
|
||||
login (password) {
|
||||
return fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: objectToParams({ password }),
|
||||
...this.options
|
||||
}).then(response => (response.ok))
|
||||
},
|
||||
|
||||
logout () {
|
||||
return fetch('/api/logout', this.options).then(response => (response.ok))
|
||||
},
|
||||
|
||||
getVersion () {
|
||||
return fetch('/api/versions', this.options).then(response => handleResponse(response))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,17 +7,16 @@ import store from './plugins/store'
|
|||
|
||||
import * as globalsComponents from './components/globals'
|
||||
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// Register global components
|
||||
for (let component of Object.values(globalsComponents)) {
|
||||
Vue.component(component.name, component)
|
||||
for (const component of Object.values(globalsComponents)) {
|
||||
Vue.component(component.name, component)
|
||||
}
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
router,
|
||||
store,
|
||||
render: h => h(App),
|
||||
i18n,
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
|
|
|
@ -1,35 +1,34 @@
|
|||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
function loadLocaleMessages () {
|
||||
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach(key => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1]
|
||||
messages[locale] = locales(key)
|
||||
}
|
||||
})
|
||||
return messages
|
||||
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach(key => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
if (matched && matched.length > 1) {
|
||||
const locale = matched[1]
|
||||
messages[locale] = locales(key)
|
||||
}
|
||||
})
|
||||
return messages
|
||||
}
|
||||
|
||||
function getBrowserLocale() {
|
||||
const navigatorLocale = navigator.languages !== undefined
|
||||
? navigator.languages[0]
|
||||
: navigator.language
|
||||
function getBrowserLocale () {
|
||||
const navigatorLocale = navigator.languages !== undefined
|
||||
? navigator.languages[0]
|
||||
: navigator.language
|
||||
|
||||
return !navigatorLocale
|
||||
? process.env.VUE_APP_I18N_LOCALE || 'en'
|
||||
: navigatorLocale
|
||||
return !navigatorLocale
|
||||
? process.env.VUE_APP_I18N_LOCALE || 'en'
|
||||
: navigatorLocale
|
||||
}
|
||||
|
||||
export default new VueI18n({
|
||||
locale: getBrowserLocale(),
|
||||
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
|
||||
// TODO : chunk locales json and lazy load them
|
||||
messages: loadLocaleMessages()
|
||||
locale: getBrowserLocale(),
|
||||
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
|
||||
// TODO : chunk locales json and lazy load them
|
||||
messages: loadLocaleMessages()
|
||||
})
|
||||
|
|
|
@ -3,24 +3,22 @@ import VueRouter from 'vue-router'
|
|||
import routes from '../routes'
|
||||
import store from './store'
|
||||
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const router = new VueRouter({
|
||||
// mode: 'history', // this allow all routes to be real ones (without '#')
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
// mode: 'history', // this allow all routes to be real ones (without '#')
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
// Before each route request hook
|
||||
router.beforeEach((to, from, next) => {
|
||||
// Allow if connected or route is not protected
|
||||
if (store.state.connected || to.meta.noAuth) {
|
||||
next()
|
||||
} else {
|
||||
next({name: 'login', query: {redirect: to.path}})
|
||||
}
|
||||
// Allow if connected or route is not protected
|
||||
if (store.state.connected || to.meta.noAuth) {
|
||||
next()
|
||||
} else {
|
||||
next({ name: 'login', query: { redirect: to.path } })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
connected: localStorage.getItem('connected') === 'true',
|
||||
yunohostInfos: null
|
||||
state: {
|
||||
connected: localStorage.getItem('connected') === 'true',
|
||||
yunohostInfos: null
|
||||
},
|
||||
// Mutations must be synchronous. They are used to change the store state.
|
||||
mutations: {
|
||||
'CONNECTED' (state, connected) {
|
||||
localStorage.setItem('connected', connected)
|
||||
state.connected = connected
|
||||
if (!connected) {
|
||||
state.yunohostInfos = null
|
||||
}
|
||||
},
|
||||
// Mutations must be synchronous. They are used to change the store state.
|
||||
mutations: {
|
||||
['CONNECTED'] (state, connected) {
|
||||
localStorage.setItem('connected', connected)
|
||||
state.connected = connected
|
||||
if (!connected) {
|
||||
state.yunohostInfos = null
|
||||
}
|
||||
},
|
||||
['YUNOHOST_INFOS'] (state, data) {
|
||||
console.log('version changed', data);
|
||||
state.yunohostInfos = data
|
||||
}
|
||||
},
|
||||
// Actions may be asynchronous. They are used to commit mutations.
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
'YUNOHOST_INFOS' (state, data) {
|
||||
console.log('version changed', data)
|
||||
state.yunohostInfos = data
|
||||
}
|
||||
},
|
||||
// Actions may be asynchronous. They are used to commit mutations.
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,21 +3,29 @@ import Login from './views/Login'
|
|||
import Users from './views/Users'
|
||||
import User from './views/User'
|
||||
|
||||
|
||||
const routes = [
|
||||
{name: 'home', path: '/', component: Home},
|
||||
{name: 'login', path: '/login', component: Login, meta: {
|
||||
noAuth: true
|
||||
}},
|
||||
{name: 'users', path: '/users', component: Users, meta: {
|
||||
breadcrumb: [{name: 'users', trad: 'users'}]
|
||||
}},
|
||||
{name: 'user', path: '/user/:name', component: User, props: true, meta: {
|
||||
breadcrumb: [
|
||||
{name: 'users', trad: 'users'},
|
||||
{name: 'user', param: 'name'}
|
||||
]
|
||||
}},
|
||||
{ name: 'home', path: '/', component: Home },
|
||||
{ name: 'login', path: '/login', component: Login, meta: { noAuth: true } },
|
||||
{
|
||||
name: 'users',
|
||||
path: '/users',
|
||||
component: Users,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'users', trad: 'users' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
path: '/user/:name',
|
||||
component: User,
|
||||
props: true,
|
||||
meta: {
|
||||
breadcrumb: [
|
||||
{ name: 'users', trad: 'users' },
|
||||
{ name: 'user', param: 'name' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
||||
|
|
|
@ -18,20 +18,20 @@
|
|||
|
||||
// Bootstrap overrides
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", "Fira Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.list-group-item {
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.list-group-item-action {
|
||||
color: #333;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
// Fork-awesome overrides
|
||||
.fa-fw {
|
||||
width: 1.25em !important;
|
||||
width: 1.25em !important;
|
||||
}
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="item in menu" :key="item.id" :to="item.uri">
|
||||
<icon :iname="item.icon" class="lg"/>
|
||||
<h2>{{ $t(item.translation) }}</h2>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto"/>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
<div class="home">
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="item in menu" :key="item.id" :to="item.uri">
|
||||
<icon :iname="item.icon" class="lg" />
|
||||
<h2>{{ $t(item.translation) }}</h2>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home',
|
||||
data: () => {
|
||||
return {
|
||||
menu: [
|
||||
{id: 0, uri: '/users', icon: 'users', translation: 'users'},
|
||||
{id: 1, uri: '/domains', icon: 'globe', translation: 'domains'},
|
||||
{id: 2, uri: '/apps', icon: 'cubes', translation: 'applications'},
|
||||
{id: 3, uri: '/update', icon: 'refresh', translation: 'system_update'},
|
||||
{id: 4, uri: '/services', icon: 'cog', translation: 'services'},
|
||||
{id: 5, uri: '/tools', icon: 'wrench', translation: 'tools'},
|
||||
{id: 6, uri: '/diagnosis', icon: 'stethoscope', translation: 'diagnosis'},
|
||||
{id: 7, uri: '/backup', icon: 'archive', translation: 'backup'},
|
||||
]
|
||||
}
|
||||
},
|
||||
name: 'Home',
|
||||
data: () => {
|
||||
return {
|
||||
menu: [
|
||||
{ id: 0, uri: '/users', icon: 'users', translation: 'users' },
|
||||
{ id: 1, uri: '/domains', icon: 'globe', translation: 'domains' },
|
||||
{ id: 2, uri: '/apps', icon: 'cubes', translation: 'applications' },
|
||||
{ id: 3, uri: '/update', icon: 'refresh', translation: 'system_update' },
|
||||
{ id: 4, uri: '/services', icon: 'cog', translation: 'services' },
|
||||
{ id: 5, uri: '/tools', icon: 'wrench', translation: 'tools' },
|
||||
{ id: 6, uri: '/diagnosis', icon: 'stethoscope', translation: 'diagnosis' },
|
||||
{ id: 7, uri: '/backup', icon: 'archive', translation: 'backup' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,58 +1,59 @@
|
|||
<template>
|
||||
<b-form @submit.prevent="login">
|
||||
<!-- TODO add hidden domain input -->
|
||||
<b-input-group>
|
||||
<template v-slot:prepend>
|
||||
<b-input-group-text>
|
||||
<label class="sr-only" for="input-password" >{{ $t('password') }}</label>
|
||||
<icon iname="lock" class="sm"/>
|
||||
</b-input-group-text>
|
||||
</template>
|
||||
<b-form-input required id="input-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
:placeholder="$t('administration_password')"
|
||||
:state="isValid"
|
||||
></b-form-input>
|
||||
<template v-slot:append>
|
||||
<b-button type="submit" variant="success">{{ $t('login') }}</b-button>
|
||||
</template>
|
||||
</b-input-group>
|
||||
<b-form-invalid-feedback :state="isValid">
|
||||
{{ $t('wrong_password') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form>
|
||||
<b-form @submit.prevent="login">
|
||||
<!-- TODO add hidden domain input -->
|
||||
<b-input-group>
|
||||
<template v-slot:prepend>
|
||||
<b-input-group-text>
|
||||
<label class="sr-only" for="input-password">{{ $t('password') }}</label>
|
||||
<icon iname="lock" class="sm" />
|
||||
</b-input-group-text>
|
||||
</template>
|
||||
<b-form-input
|
||||
id="input-password"
|
||||
required type="password"
|
||||
v-model="password"
|
||||
:placeholder="$t('administration_password')" :state="isValid"
|
||||
/>
|
||||
<template v-slot:append>
|
||||
<b-button type="submit" variant="success">
|
||||
{{ $t('login') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-input-group>
|
||||
<b-form-invalid-feedback :state="isValid">
|
||||
{{ $t('wrong_password') }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/helpers/api'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data: () => {
|
||||
return {
|
||||
password: '',
|
||||
isValid: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
const connected = await api.login(this.password)
|
||||
if (connected) {
|
||||
this.$store.commit('CONNECTED', true);
|
||||
this.$router.push(this.$route.query.redirect || '/')
|
||||
const infos = await api.getVersion();
|
||||
this.$store.commit('YUNOHOST_INFOS', infos.yunohost)
|
||||
} else {
|
||||
this.$store.commit('CONNECTED', false);
|
||||
this.isValid = false
|
||||
}
|
||||
}
|
||||
},
|
||||
// TODO checkInstall
|
||||
// beforeRouteEnter (to, from, next) {
|
||||
// },
|
||||
name: 'Login',
|
||||
data: () => {
|
||||
return {
|
||||
password: '',
|
||||
isValid: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login () {
|
||||
const connected = await api.login(this.password)
|
||||
if (connected) {
|
||||
this.$store.commit('CONNECTED', true)
|
||||
this.$router.push(this.$route.query.redirect || '/')
|
||||
const infos = await api.getVersion()
|
||||
this.$store.commit('YUNOHOST_INFOS', infos.yunohost)
|
||||
} else {
|
||||
this.$store.commit('CONNECTED', false)
|
||||
this.isValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO checkInstall
|
||||
// beforeRouteEnter (to, from, next) {
|
||||
// },
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,63 +1,73 @@
|
|||
<template>
|
||||
<div class="user">
|
||||
<breadcrumb/>
|
||||
<b-card :class="{skeleton: !user}">
|
||||
<template v-slot:header>
|
||||
<h2>{{ user ? user.fullname : '' }}</h2>
|
||||
</template>
|
||||
<div class="d-flex align-items-center">
|
||||
<icon iname="user" class="fa-fw"></icon>
|
||||
<div class="w-100">
|
||||
<template v-if="user">
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_username') }}</strong></b-col>
|
||||
<b-col>{{ user.username }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_email') }}</strong></b-col>
|
||||
<b-col class="font-italic">{{ user.mail }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].limit }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].use }}</b-col>
|
||||
</b-row>
|
||||
<b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType">
|
||||
<b-col><strong>{{ $t(trad) }}</strong></b-col>
|
||||
<b-col>
|
||||
<ul v-if="user[mailType] && user[mailType].length > 1">
|
||||
<li v-for="(alias, index) in user[mailType]" :key="index">
|
||||
{{ alias }}
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else-if="user[mailType]">
|
||||
{{ user[mailType] }}
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<!-- skeleton -->
|
||||
<template v-else>
|
||||
<b-row v-for="(n, index) in 6" :key="index">
|
||||
<b-col><strong class="rounded"></strong></b-col>
|
||||
<b-col><span v-if="n <= 4" class="rounded"></span></b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-slot:footer >
|
||||
<div class="d-flex d-flex justify-content-end">
|
||||
<b-button :to="user ? {name: 'user-edit', params: {user: user}} : null" :variant="user ? 'info' : 'dark'" >
|
||||
{{ user ? $t('user_username_edit', {name: user.username}) : '' }}
|
||||
</b-button>
|
||||
<b-button :variant="user ? 'danger' : 'dark'" class="ml-2">{{ user ? $t('delete') : '' }}</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-card>
|
||||
</div>
|
||||
<div class="user">
|
||||
<breadcrumb />
|
||||
<b-card :class="{skeleton: !user}">
|
||||
<template v-slot:header>
|
||||
<h2>{{ user ? user.fullname : '' }}</h2>
|
||||
</template>
|
||||
<div class="d-flex align-items-center">
|
||||
<icon iname="user" class="fa-fw" />
|
||||
<div class="w-100">
|
||||
<template v-if="user">
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_username') }}</strong></b-col>
|
||||
<b-col>{{ user.username }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_email') }}</strong></b-col>
|
||||
<b-col class="font-italic">
|
||||
{{ user.mail }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_quota') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].limit }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col><strong>{{ $t('user_mailbox_use') }}</strong></b-col>
|
||||
<b-col>{{ user['mailbox-quota'].use }}</b-col>
|
||||
</b-row>
|
||||
<b-row v-for="(trad, mailType) in {'mail-aliases': 'user_emailaliases', 'mail-forward': 'user_emailforward'}" :key="mailType">
|
||||
<b-col><strong>{{ $t(trad) }}</strong></b-col>
|
||||
<b-col>
|
||||
<ul v-if="user[mailType] && user[mailType].length > 1">
|
||||
<li v-for="(alias, index) in user[mailType]" :key="index">
|
||||
{{ alias }}
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else-if="user[mailType]">
|
||||
{{ user[mailType] }}
|
||||
</template>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<!-- skeleton -->
|
||||
<template v-else>
|
||||
<b-row v-for="(n, index) in 6" :key="index">
|
||||
<b-col>
|
||||
<strong class="rounded" />
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span v-if="n <= 4" class="rounded" />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-slot:footer>
|
||||
<div class="d-flex d-flex justify-content-end">
|
||||
<b-button :to="user ? {name: 'user-edit', params: {user: user}} : null"
|
||||
:variant="user ? 'info' : 'dark'"
|
||||
>
|
||||
{{ user ? $t('user_username_edit', {name: user.username}) : '' }}
|
||||
</b-button>
|
||||
<b-button :variant="user ? 'danger' : 'dark'" class="ml-2">
|
||||
{{ user ? $t('delete') : '' }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -65,21 +75,26 @@ import api from '@/helpers/api'
|
|||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
|
||||
export default {
|
||||
name: 'User',
|
||||
props: ['name'],
|
||||
data: function () {
|
||||
return {
|
||||
user: undefined,
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const data = await api.get('users/' + this.name)
|
||||
if (!data) return;
|
||||
this.user = data
|
||||
},
|
||||
components: {
|
||||
Breadcrumb
|
||||
name: 'User',
|
||||
props: {
|
||||
name: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
user: undefined
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
const data = await api.get('users/' + this.name)
|
||||
if (!data) return
|
||||
this.user = data
|
||||
},
|
||||
components: {
|
||||
Breadcrumb
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -87,68 +102,67 @@ export default {
|
|||
@import '@/scss/main.scss';
|
||||
|
||||
.card-body > div {
|
||||
flex-direction: column;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
flex-direction: column;
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon.fa-user {
|
||||
font-size: 10rem;
|
||||
padding-right: 3rem;
|
||||
padding-left: 1.75rem;
|
||||
font-size: 10rem;
|
||||
padding-right: 3rem;
|
||||
padding-left: 1.75rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
+ .row {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
+ .row {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
padding: .5rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
font-style: italic;
|
||||
list-style: none;
|
||||
}
|
||||
li {
|
||||
font-style: italic;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
|
||||
h2 {
|
||||
height: #{2 * 1.2}rem;
|
||||
h2 {
|
||||
height: #{2 * 1.2}rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
& > * {
|
||||
display: block;
|
||||
background-color: $skeleton-color;
|
||||
height: 1.5rem;
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
& > * {
|
||||
display: block;
|
||||
background-color: $skeleton-color;
|
||||
height: 1.5rem;
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
max-width: 12rem;
|
||||
}
|
||||
strong {
|
||||
max-width: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
height: calc(2.25rem + 2px);
|
||||
width: 7rem;
|
||||
}
|
||||
button {
|
||||
height: calc(2.25rem + 2px);
|
||||
width: 7rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,32 +1,34 @@
|
|||
<template>
|
||||
<div class="users">
|
||||
<breadcrumb/>
|
||||
<template v-if="users === null">
|
||||
<b-alert variant="warning" show>
|
||||
<icon iname="exclamation-triangle" class="fa-fw mr-1"/>
|
||||
{{ $t('users_no') }}
|
||||
</b-alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
<b-list-group :class="{skeleton: !users}">
|
||||
<b-list-group-item
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
v-for="(user, index) in (users ? users : 3)"
|
||||
:key="index"
|
||||
:to="users ? { name: 'user', params: { name: user.username }} : null"
|
||||
>
|
||||
<div>
|
||||
<h5 :class="{rounded: !users}">
|
||||
{{ user.username }}
|
||||
<small>({{ user.fullname }})</small>
|
||||
</h5>
|
||||
<p :class="{rounded: !users}">{{ user.mail }}</p>
|
||||
</div>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto"/>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</template>
|
||||
</div>
|
||||
<div class="users">
|
||||
<breadcrumb />
|
||||
<template v-if="users === null">
|
||||
<b-alert variant="warning" show>
|
||||
<icon iname="exclamation-triangle" class="fa-fw mr-1" />
|
||||
{{ $t('users_no') }}
|
||||
</b-alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
<b-list-group :class="{skeleton: !users}">
|
||||
<b-list-group-item
|
||||
v-for="(user, index) in (users ? users : 3)"
|
||||
:key="index"
|
||||
:to="users ? { name: 'user', params: { name: user.username }} : null"
|
||||
class="d-flex justify-content-between align-items-center pr-0"
|
||||
>
|
||||
<div>
|
||||
<h5 :class="{rounded: !users}">
|
||||
{{ user.username }}
|
||||
<small>({{ user.fullname }})</small>
|
||||
</h5>
|
||||
<p :class="{rounded: !users}">
|
||||
{{ user.mail }}
|
||||
</p>
|
||||
</div>
|
||||
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -34,25 +36,25 @@ import api from '@/helpers/api'
|
|||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
data: function () {
|
||||
return {
|
||||
users: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
async created() {
|
||||
const data = await api.get('users')
|
||||
if (!data || Object.keys(data.users).length === 0) {
|
||||
this.users = null
|
||||
} else {
|
||||
this.users = data.users
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Breadcrumb
|
||||
},
|
||||
name: 'Users',
|
||||
data: function () {
|
||||
return {
|
||||
users: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
async created () {
|
||||
const data = await api.get('users')
|
||||
if (!data || Object.keys(data.users).length === 0) {
|
||||
this.users = null
|
||||
} else {
|
||||
this.users = data.users
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Breadcrumb
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -60,22 +62,22 @@ export default {
|
|||
@import '@/scss/_variables.scss';
|
||||
|
||||
p {
|
||||
margin: 0
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
@each $i, $opacity in 1 .75, 2 .5, 3 .25 {
|
||||
.list-group-item:nth-child(#{$i}) { opacity: $opacity; }
|
||||
}
|
||||
@each $i, $opacity in 1 .75, 2 .5, 3 .25 {
|
||||
.list-group-item:nth-child(#{$i}) { opacity: $opacity; }
|
||||
}
|
||||
|
||||
h5, p {
|
||||
background-color: $skeleton-color;
|
||||
height: 1.5rem;
|
||||
width: 10rem;
|
||||
}
|
||||
h5, p {
|
||||
background-color: $skeleton-color;
|
||||
height: 1.5rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Reference in a new issue