Update views with ViewTopBar and SearchVue components

This commit is contained in:
Axolotle 2020-12-04 18:26:11 +01:00
parent 2b80a0b1e0
commit 04ad65761e
11 changed files with 354 additions and 402 deletions

View file

@ -103,6 +103,7 @@
"created_at": "Created at",
"custom_app_install": "Install custom app",
"custom_app_url_only_github": "Currently only from GitHub",
"day_validity": " Expired | 1 day | {count} days",
"dead": "Inactive",
"delete": "Delete",
"description": "Description",
@ -217,6 +218,17 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"issues": "{count} issues",
"items": {
"apps": "no apps | app | {c} apps",
"backups": "no backups | backup | {c} backups",
"domains": "no domains | domain | {c} domains",
"groups": "no groups | group | {c} groups",
"installed_apps": "no installed apps | installed app | {c} installed apps",
"logs": "no logs | log | {c} logs",
"services": "no services | service | {c} services",
"users": "no users | user | {c} users"
},
"items_verbose_count": "There is {items}.",
"label": "Label",
"label_for_manifestname": "Label for {name}",
"last_ran": "Last time ran:",
@ -264,9 +276,6 @@
"groupname": "My group name",
"domain": "my-domain.com"
},
"pluralized": {
"day_validity": " Expired | 1 day | {count} days"
},
"logs": "Logs",
"logs_suboperations": "Sub-operations",
"logs_operation": "Operations made on system with YunoHost",
@ -307,15 +316,8 @@
"running": "Running",
"save": "Save",
"search": {
"domain": "Search for domains...",
"group": "Search for groups...",
"installed_app": "Search for installed apps...",
"service": "Search for services",
"user": "Search for users...",
"logs": "Search in logs...",
"not_found": {
"installed_app": "There is no apps matching your search query."
}
"for": "Search for {items}...",
"not_found": "There is {items} matching your criteria."
},
"search_for_apps": "Search for apps...",
"select_all": "Select all",

View file

@ -148,7 +148,10 @@ export default {
},
getters: {
users: state => state.users,
users: state => {
if (state.users) return Object.values(state.users)
return state.users
},
userNames: state => {
if (state.users) return Object.keys(state.users)

View file

@ -1,55 +1,43 @@
<template>
<div class="app-list">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input
:disabled="!apps"
id="search-app" v-model="search"
:placeholder="$t('search.installed_app')"
/>
</b-input-group>
<div class="buttons">
<b-button variant="success" :to="{ name: 'app-catalog' }">
<icon iname="plus" /> {{ $t('install') }}
</b-button>
</div>
</div>
<template v-if="apps !== undefined">
<b-alert v-if="apps === null" variant="warning" show>
<icon iname="exclamation-triangle" /> {{ $t('no_installed_apps') }}
</b-alert>
<b-list-group v-else-if="filteredApps && filteredApps.length">
<b-list-group-item
v-for="{ id, name, description, label } in filteredApps" :key="id"
:to="{ name: 'app-info', params: { id }}"
class="d-flex justify-content-between align-items-center pr-0"
>
<div>
<h5 class="font-weight-bold">{{ label }}
<small v-if="name" class="text-secondary">{{ name }}</small>
</h5>
<p class="m-0">
{{ description }}
</p>
</div>
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
</b-list-group>
<b-alert v-else variant="warning" show>
<icon iname="exclamation-triangle" /> {{ $t('search.not_found.installed_app') }}
</b-alert>
<search-view
id="app-list"
:search.sync="search"
:items="apps"
:filtered-items="filteredApps"
items-name="installed_apps"
>
<template #top-bar-buttons>
<b-button variant="success" :to="{ name: 'app-catalog' }">
<icon iname="plus" />
{{ $t('install') }}
</b-button>
</template>
</div>
<b-list-group>
<b-list-group-item
v-for="{ id, name, description, label } in filteredApps" :key="id"
:to="{ name: 'app-info', params: { id }}"
class="d-flex justify-content-between align-items-center pr-0"
>
<div>
<h5 class="font-weight-bold">
{{ label }}
<small v-if="name" class="text-secondary">{{ name }}</small>
</h5>
<p class="m-0">
{{ description }}
</p>
</div>
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
</b-list-group>
</search-view>
</template>
<script>
import api from '@/api'
import SearchView from '@/components/SearchView'
export default {
name: 'AppList',
@ -65,9 +53,10 @@ export default {
filteredApps () {
if (!this.apps) return
const search = this.search.toLowerCase()
const match = (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.
return 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
}
},
@ -107,10 +96,8 @@ export default {
created () {
this.fetchData()
}
},
components: { SearchView }
}
</script>
<style>
</style>

View file

@ -1,16 +1,12 @@
<template>
<div class="backup-list">
<div class="actions">
<div class="buttons ml-auto">
<b-button variant="success" :to="{ name: 'backup-create' }">
<icon iname="plus" /> {{ $t('backup_new') }}
</b-button>
</div>
</div>
<view-top-bar :button="{ text: $t('backup_new'), icon: 'plus', to: { name: 'backup-create' } }" />
<b-alert v-if="!archives" variant="warning" show>
<icon iname="exclamation-triangle" /> {{ $t('backups_no') }}
<icon iname="exclamation-triangle" />
{{ $t('items_verbose_count', { items: $tc('items.backups', 0) }) }}
</b-alert>
<b-list-group v-else>
<b-list-group-item
v-for="{ name, created_at, path, size } in archives" :key="name"
@ -23,7 +19,9 @@
{{ created_at | distanceToNow }}
<small>{{ name }} ({{ size | humanSize }})</small>
</h5>
<p class="mb-0">{{ path }}</p>
<p class="mb-0">
{{ path }}
</p>
</div>
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
@ -52,19 +50,14 @@ export default {
}
},
filters: {
distanceToNow,
readableDate,
humanSize
},
methods: {
fetchData () {
api.get('backup/archives?with_info').then(({ archives }) => {
api.get('backup/archives?with_info').then(data => {
// FIXME use archives = null if no archives
this.archives = Object.entries(archives).map(([name, data]) => {
data.name = name
return data
const archives = Object.entries(data.archives)
this.archives = archives.length === 0 ? null : archives.map(([name, infos]) => {
infos.name = name
return infos
}).reverse()
})
}
@ -72,6 +65,12 @@ export default {
created () {
this.fetchData()
},
filters: {
distanceToNow,
readableDate,
humanSize
}
}
</script>

View file

@ -1,12 +1,12 @@
<template>
<div class="diagnosis">
<div class="actions">
<div class="buttons ml-auto">
<b-button @click="shareLogs">
<view-top-bar>
<template #group-right>
<b-button @click="shareLogs" variant="success">
<icon iname="cloud-upload" /> {{ $t('logs_share_with_yunopaste') }}
</b-button>
</div>
</div>
</template>
</view-top-bar>
<b-alert variant="info" show>
{{ $t(reports ? 'diagnosis_explanation' : 'diagnosis_first_run') }}
@ -119,10 +119,6 @@ export default {
}
},
filters: {
distanceToNow
},
methods: {
fetchData () {
api.get('diagnosis/show?full').then((data) => {
@ -196,6 +192,10 @@ export default {
created () {
api.post('diagnosis/run?except_if_never_ran_yet').then(this.fetchData)
},
filters: {
distanceToNow
}
}
</script>

View file

@ -14,7 +14,7 @@
<dd>{{ cert.type }} ({{ name }})</dd>
<hr>
<dt v-t="'validity'" />
<dd>{{ $tc('pluralized.day_validity', cert.validity) }}</dd>
<dd>{{ $tc('day_validity', cert.validity) }}</dd>
</dl>
</b-card>
@ -82,6 +82,7 @@ import api from '@/api'
export default {
name: 'DomainCert',
props: {
name: {
type: String,
@ -176,6 +177,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,20 +1,19 @@
<template>
<div class="domain-list">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input id="search-domain" v-model="search" :placeholder="$t('search.domain')" />
</b-input-group>
<div class="buttons">
<b-button variant="success" :to="{name: 'domain-add'}">
<icon iname="plus" /> {{ $t('domain_add') }}
</b-button>
</div>
</div>
<search-view
id="domain-list"
:search.sync="search"
:items="domains"
:filtered-items="filteredDomains"
items-name="domains"
>
<template #top-bar-buttons>
<b-button variant="success" :to="{ name: 'domain-add' }">
<icon iname="plus" />
{{ $t('domain_add') }}
</b-button>
</template>
<b-list-group v-if="filteredDomains">
<b-list-group>
<b-list-group-item
v-for="domain in filteredDomains" :key="domain"
:to="{ name: 'domain-info', params: { name: domain }}"
@ -28,39 +27,42 @@
<icon iname="star" :title="$t('words.default')" />
</small>
</h5>
<p class="font-italic">https://{{ domain }}</p>
<p class="font-italic m-0">
https://{{ domain }}
</p>
</div>
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
</b-list-group>
</div>
</search-view>
</template>
<script>
import { mapGetters } from 'vuex'
import SearchView from '@/components/SearchView'
export default {
name: 'DomainList',
data: () => ({
search: ''
}),
computed: {
filteredDomains () {
const domains = this.$store.state.data.domains
const mainDomain = this.mainDomain
if (!domains || !mainDomain) return
const search = this.search.toLowerCase()
return domains
.filter(name => name.toLowerCase().includes(search))
.sort(prevDomain => prevDomain === mainDomain ? -1 : 1)
},
mainDomain () {
return this.$store.state.data.main_domain
data () {
return {
search: ''
}
},
methods: {
computed: {
...mapGetters(['domains', 'mainDomain']),
filteredDomains () {
if (!this.domains || !this.mainDomain) return
const search = this.search.toLowerCase()
const mainDomain = this.mainDomain
const domains = this.domains
.filter(name => name.toLowerCase().includes(search))
.sort(prevDomain => prevDomain === mainDomain ? -1 : 1)
return domains.length > 0 ? domains : null
}
},
created () {
@ -68,28 +70,8 @@ export default {
{ uri: 'domains/main', storeKey: 'main_domain' },
{ uri: 'domains' }
])
}
},
components: { SearchView }
}
</script>
<style lang="scss" scoped>
p {
margin: 0
}
.skeleton {
@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;
}
small {
display: none;
}
}
</style>

View file

@ -1,170 +1,171 @@
<template lang="html">
<div class="group-list">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input id="search-group" v-model="search" :placeholder="$t('search.group')" />
</b-input-group>
<div class="buttons">
<b-button variant="success" :to="{name: 'group-create'}">
<icon iname="plus" /> {{ $t('group_new') }}
</b-button>
</div>
</div>
<!-- PRIMARY GROUPS CARDS -->
<template v-if="normalGroups">
<b-card
v-for="(group, name, index) in filteredGroups" :key="name"
no-body
>
<b-card-header class="d-flex align-items-center">
<h2>
<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
v-if="!group.isSpecial" v-b-modal.delete-modal
variant="danger" class="ml-2" size="sm"
@click="groupToDelete = name"
>
<icon :title="$t('delete')" iname="trash-o" /> <span class="sr-only">{{ $t('delete') }}</span>
</b-button>
</div>
</b-card-header>
<b-collapse :id="'collapse-' + index" visible>
<b-card-body>
<b-row>
<b-col md="3" lg="2">
<strong>{{ $t('users') }}</strong>
</b-col>
<b-col>
<template v-if="group.isSpecial">
<p><icon iname="info-circle" /> {{ $t('group_explain_' + name) }}</p>
<p v-if="name === 'visitors'">
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
</p>
</template>
<template v-else>
<zone-selectize
:choices="group.availableMembers" :selected="group.members"
item-icon="user"
:label="$t('group_add_member')"
@change="onUserChanged({ ...$event, name })"
/>
</template>
</b-col>
</b-row>
<hr>
<b-row>
<b-col md="3" lg="2">
<strong>{{ $t('permissions') }}</strong>
</b-col>
<b-col>
<zone-selectize
item-icon="key-modern" item-variant="dark"
:choices="group.availablePermissions"
:selected="group.permissions"
:label="$t('group_add_permission')"
:format="formatPermission"
:removable="name === 'visitors' ? removable : null"
@change="onPermissionChanged({ ...$event, name, groupType: 'normal' })"
/>
</b-col>
</b-row>
</b-card-body>
</b-collapse>
</b-card>
<template>
<search-view
id="group-list"
:search.sync="search"
:items="normalGroups"
:filtered-items="filteredGroups"
items-name="groups"
>
<template #top-bar-buttons>
<b-button variant="success" :to="{ name: 'group-create' }">
<icon iname="plus" />
{{ $t('group_new') }}
</b-button>
</template>
<!-- GROUP SPECIFIC CARD -->
<b-card no-body v-if="userGroups">
<!-- PRIMARY GROUPS CARDS -->
<b-card
v-for="(group, name, index) in filteredGroups" :key="name"
no-body
>
<b-card-header class="d-flex align-items-center">
<h2>
<icon iname="group" /> {{ $t('group_specific_permissions') }}
<icon iname="group" /> {{ group.isSpecial ? $t('group_' + name) : `${$t('group')} "${name}"` }}
</h2>
<div class="ml-auto">
<b-button v-b-toggle.collapse-specific size="sm" variant="outline-secondary">
<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
v-if="!group.isSpecial" v-b-modal.delete-modal
variant="danger" class="ml-2" size="sm"
@click="groupToDelete = name"
>
<icon :title="$t('delete')" iname="trash-o" /> <span class="sr-only">{{ $t('delete') }}</span>
</b-button>
</div>
</b-card-header>
<b-collapse id="collapse-specific" visible>
<b-collapse :id="'collapse-' + index" visible>
<b-card-body>
<div v-for="name in userGroupsNames" :key="name">
<b-row>
<b-col md="3" lg="2">
<icon iname="user" /> <strong>{{ name }}</strong>
</b-col>
<b-row>
<b-col md="3" lg="2">
<strong>{{ $t('users') }}</strong>
</b-col>
<b-col>
<b-col>
<template v-if="group.isSpecial">
<p><icon iname="info-circle" /> {{ $t('group_explain_' + name) }}</p>
<p v-if="name === 'visitors'">
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
</p>
</template>
<template v-else>
<zone-selectize
item-icon="key-modern" item-variant="dark"
:choices="userGroups[name].availablePermissions"
:selected="userGroups[name].permissions"
:label="$t('group_add_permission')"
:format="formatPermission"
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
:choices="group.availableMembers" :selected="group.members"
item-icon="user"
:label="$t('group_add_member')"
@change="onUserChanged({ ...$event, name })"
/>
</b-col>
</b-row>
<hr>
</div>
<base-selectize
v-if="availableMembers.length"
:label="$t('group_add_member')"
:choices="availableMembers"
:selected="userGroupsNames"
@selected="onSpecificUserAdded"
/>
</template>
</b-col>
</b-row>
<hr>
<b-row>
<b-col md="3" lg="2">
<strong>{{ $t('permissions') }}</strong>
</b-col>
<b-col>
<zone-selectize
item-icon="key-modern" item-variant="dark"
:choices="group.availablePermissions"
:selected="group.permissions"
:label="$t('group_add_permission')"
:format="formatPermission"
:removable="name === 'visitors' ? removable : null"
@change="onPermissionChanged({ ...$event, name, groupType: 'normal' })"
/>
</b-col>
</b-row>
</b-card-body>
</b-collapse>
</b-card>
<!-- 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>
</div>
<!-- GROUP SPECIFIC CARD -->
<template #extra>
<b-card no-body v-if="userGroups">
<b-card-header class="d-flex align-items-center">
<h2>
<icon iname="group" /> {{ $t('group_specific_permissions') }}
</h2>
<div class="ml-auto">
<b-button v-b-toggle.collapse-specific size="sm" variant="outline-secondary">
<icon iname="chevron-right" class="rotate" /><span class="sr-only">{{ $t('words.collapse') }}</span>
</b-button>
</div>
</b-card-header>
<b-collapse id="collapse-specific" visible>
<b-card-body>
<div v-for="name in userGroupsNames" :key="name">
<b-row>
<b-col md="3" lg="2">
<icon iname="user" /> <strong>{{ name }}</strong>
</b-col>
<b-col>
<zone-selectize
item-icon="key-modern" item-variant="dark"
:choices="userGroups[name].availablePermissions"
:selected="userGroups[name].permissions"
:label="$t('group_add_permission')"
:format="formatPermission"
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
/>
</b-col>
</b-row>
<hr>
</div>
<base-selectize
v-if="availableMembers.length"
:label="$t('group_add_member')"
:choices="availableMembers"
:selected="userGroupsNames"
@selected="onSpecificUserAdded"
/>
</b-card-body>
</b-collapse>
</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>
<script>
import Vue from 'vue'
import api from '@/api'
import { isEmptyValue } from '@/helpers/commons'
import SearchView from '@/components/SearchView'
import ZoneSelectize from '@/components/ZoneSelectize'
import BaseSelectize from '@/components/BaseSelectize'
// TODO add global search with type (search by: group, user, permission)
// TODO add vuex store update on inputs ?
export default {
name: 'GroupList',
data: () => ({
search: '',
permissions: undefined,
normalGroups: undefined,
userGroups: undefined,
groupToDelete: undefined
}),
data () {
return {
search: '',
permissions: undefined,
normalGroups: undefined,
userGroups: undefined,
groupToDelete: undefined
}
},
computed: {
filteredGroups () {
@ -177,7 +178,7 @@ export default {
filtered[name] = groups[name]
}
}
return filtered
return isEmptyValue(filtered) ? null : filtered
},
userGroupsNames () {
@ -254,7 +255,7 @@ export default {
// updates while modifying values.
const normalGroups = {}
const userGroups = {}
const userNames = Object.keys(users)
const userNames = users ? Object.keys(users) : []
for (const groupName in allGroups) {
// copy the group to unlink it from the store
@ -293,6 +294,7 @@ export default {
},
components: {
SearchView,
ZoneSelectize,
BaseSelectize
}

View file

@ -1,14 +1,11 @@
<template>
<div class="service-list">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input id="search-service" v-model="search" :placeholder="$t('search.service')" />
</b-input-group>
</div>
<search-view
id="service-list"
:search.sync="search"
:items="services"
:filtered-items="filteredServices"
items-name="services"
>
<b-list-group v-if="filteredServices">
<b-list-group-item
v-for="{ name, description, status, last_state_change } in filteredServices"
@ -17,7 +14,10 @@
class="d-flex justify-content-between align-items-center pr-0"
>
<div class="w-100">
<h5 class="font-weight-bold">{{ name }} <small class="text-secondary">{{ description }}</small></h5>
<h5 class="font-weight-bold">
{{ name }}
<small class="text-secondary">{{ description }}</small>
</h5>
<p class="mb-0">
<span :class="status === 'running' ? 'text-success' : 'text-danger'">
<icon :iname="status === 'running' ? 'check-circle' : 'times'" />
@ -29,17 +29,18 @@
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
</b-list-group>
</div>
</search-view>
</template>
<script>
import api from '@/api'
import { distanceToNow } from '@/helpers/filters/date'
import SearchView from '@/components/SearchView'
export default {
name: 'ServiceList',
data: function () {
data () {
return {
search: '',
services: undefined
@ -50,16 +51,13 @@ export default {
filteredServices () {
if (!this.services) return
const search = this.search.toLowerCase()
return this.services.filter(({ name }) => {
const services = this.services.filter(({ name }) => {
return name.toLowerCase().includes(search)
})
return services.length > 0 ? services : null
}
},
filters: {
distanceToNow
},
methods: {
fetchData () {
// simply use the api helper since we will not store the request's result.
@ -77,6 +75,12 @@ export default {
created () {
this.fetchData()
},
components: { SearchView },
filters: {
distanceToNow
}
}
</script>

View file

@ -1,15 +1,12 @@
<template>
<div class="tool-logs">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input id="search-logs" v-model="search" :placeholder="$t('search.logs')" />
</b-input-group>
</div>
<b-card no-body v-if="operations">
<search-view
id="tool-logs"
:search.sync="search"
:items="operations"
:filtered-items="filteredOperations"
items-name="logs"
>
<b-card no-body>
<template v-slot:header>
<h2><icon iname="wrench" /> {{ $t('logs_operation') }}</h2>
</template>
@ -25,18 +22,18 @@
</b-list-group-item>
</b-list-group>
</b-card>
</div>
</search-view>
</template>
<script>
import api from '@/api'
import { distanceToNow, readableDate } from '@/helpers/filters/date'
import SearchView from '@/components/SearchView'
export default {
name: 'ServiceList',
data: function () {
data () {
return {
search: '',
operations: undefined
@ -47,9 +44,10 @@ export default {
filteredOperations () {
if (!this.operations) return
const search = this.search.toLowerCase()
return this.operations.filter(({ description }) => {
const operations = this.operations.filter(({ description }) => {
return description.toLowerCase().includes(search)
})
return operations.length > 0 ? operations : null
}
},
@ -80,6 +78,8 @@ export default {
created () {
this.fetchData()
}
},
components: { SearchView }
}
</script>

View file

@ -1,100 +1,75 @@
<template>
<div class="user-list">
<div class="actions">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search" />
</b-input-group-prepend>
<b-form-input id="search-user" v-model="search" :placeholder="$t('search.user')" />
</b-input-group>
<div class="buttons">
<b-button variant="info" :to="{ name: 'group-list'}">
<icon iname="key-modern" />
{{ $t('groups_and_permissions_manage') }}
</b-button>
<search-view
id="user-list"
:search.sync="search"
:items="users"
:filtered-items="filteredUsers"
items-name="users"
>
<template #top-bar-buttons>
<b-button variant="info" :to="{ name: 'group-list' }">
<icon iname="key-modern" />
{{ $t('groups_and_permissions_manage') }}
</b-button>
<b-button variant="success" :to="{name: 'user-create'}">
<icon iname="plus" />
{{ $t('users_new') }}
</b-button>
</div>
</div>
<template v-if="users === null">
<b-alert variant="warning" show>
<icon iname="exclamation-triangle" class="fa-fw mr-1" />
{{ $t('users_no') }}
</b-alert>
<b-button variant="success" :to="{ name: 'user-create' }">
<icon iname="plus" />
{{ $t('users_new') }}
</b-button>
</template>
<template v-else>
<b-list-group :class="{skeleton: !users}">
<b-list-group-item
v-for="(user, index) in (users ? filteredUser : 3)"
:key="index"
:to="users ? { name: 'user-info', params: { name: user.username }} : null"
class="d-flex justify-content-between align-items-center pr-0"
>
<div>
<h5 :class="{rounded: !users}" class="font-weight-bold">
{{ user.username }}
<small class="text-secondary">({{ 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>
<b-list-group>
<b-list-group-item
v-for="user in filteredUsers" :key="user.username"
:to="{ name: 'user-info', params: { name: user.username }}"
class="d-flex justify-content-between align-items-center pr-0"
>
<div>
<h5 class="font-weight-bold">
{{ user.username }}
<small class="text-secondary">({{ user.fullname }})</small>
</h5>
<p class="m-0">
{{ user.mail }}
</p>
</div>
<icon iname="chevron-right" class="lg fs-sm ml-auto" />
</b-list-group-item>
</b-list-group>
</search-view>
</template>
<script>
import { mapGetters } from 'vuex'
import SearchView from '@/components/SearchView'
export default {
name: 'UserList',
data: function () {
data () {
return {
search: ''
}
},
computed: {
users () {
const users = this.$store.state.data.users
return users ? Object.values(users) : users
},
filteredUser () {
...mapGetters(['users']),
filteredUsers () {
if (!this.users) return
const search = this.search.toLowerCase()
return this.users.filter(user => {
const filtered = this.users.filter(user => {
return user.username.toLowerCase().includes(search)
})
return filtered.length === 0 ? null : filtered
}
},
created () {
this.$store.dispatch('FETCH', { uri: 'users' })
}
},
components: { SearchView }
}
</script>
<style lang="scss" scoped>
p {
margin: 0
}
.skeleton {
@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;
}
small {
display: none;
}
}
</style>