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

View file

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

View file

@ -1,55 +1,43 @@
<template> <template>
<div class="app-list"> <search-view
<div class="actions"> id="app-list"
<b-input-group> :search.sync="search"
<b-input-group-prepend is-text> :items="apps"
<icon iname="search" /> :filtered-items="filteredApps"
</b-input-group-prepend> items-name="installed_apps"
<b-form-input >
:disabled="!apps" <template #top-bar-buttons>
id="search-app" v-model="search" <b-button variant="success" :to="{ name: 'app-catalog' }">
:placeholder="$t('search.installed_app')" <icon iname="plus" />
/> {{ $t('install') }}
</b-input-group> </b-button>
<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>
</template> </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> </template>
<script> <script>
import api from '@/api' import api from '@/api'
import SearchView from '@/components/SearchView'
export default { export default {
name: 'AppList', name: 'AppList',
@ -65,9 +53,10 @@ export default {
filteredApps () { filteredApps () {
if (!this.apps) return if (!this.apps) return
const search = this.search.toLowerCase() 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. // 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 () { created () {
this.fetchData() this.fetchData()
} },
components: { SearchView }
} }
</script> </script>
<style>
</style>

View file

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

View file

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

View file

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

View file

@ -1,20 +1,19 @@
<template> <template>
<div class="domain-list"> <search-view
<div class="actions"> id="domain-list"
<b-input-group> :search.sync="search"
<b-input-group-prepend is-text> :items="domains"
<icon iname="search" /> :filtered-items="filteredDomains"
</b-input-group-prepend> items-name="domains"
<b-form-input id="search-domain" v-model="search" :placeholder="$t('search.domain')" /> >
</b-input-group> <template #top-bar-buttons>
<div class="buttons"> <b-button variant="success" :to="{ name: 'domain-add' }">
<b-button variant="success" :to="{name: 'domain-add'}"> <icon iname="plus" />
<icon iname="plus" /> {{ $t('domain_add') }} {{ $t('domain_add') }}
</b-button> </b-button>
</div> </template>
</div>
<b-list-group v-if="filteredDomains"> <b-list-group>
<b-list-group-item <b-list-group-item
v-for="domain in filteredDomains" :key="domain" v-for="domain in filteredDomains" :key="domain"
:to="{ name: 'domain-info', params: { name: domain }}" :to="{ name: 'domain-info', params: { name: domain }}"
@ -28,39 +27,42 @@
<icon iname="star" :title="$t('words.default')" /> <icon iname="star" :title="$t('words.default')" />
</small> </small>
</h5> </h5>
<p class="font-italic">https://{{ domain }}</p> <p class="font-italic m-0">
https://{{ domain }}
</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>
</div> </search-view>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import SearchView from '@/components/SearchView'
export default { export default {
name: 'DomainList', name: 'DomainList',
data: () => ({ data () {
search: '' return {
}), 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
} }
}, },
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 () { created () {
@ -68,28 +70,8 @@ export default {
{ uri: 'domains/main', storeKey: 'main_domain' }, { uri: 'domains/main', storeKey: 'main_domain' },
{ uri: 'domains' } { uri: 'domains' }
]) ])
} },
components: { SearchView }
} }
</script> </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"> <template>
<div class="group-list"> <search-view
<div class="actions"> id="group-list"
<b-input-group> :search.sync="search"
<b-input-group-prepend is-text> :items="normalGroups"
<icon iname="search" /> :filtered-items="filteredGroups"
</b-input-group-prepend> items-name="groups"
<b-form-input id="search-group" v-model="search" :placeholder="$t('search.group')" /> >
</b-input-group> <template #top-bar-buttons>
<div class="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>
</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> </template>
<!-- GROUP SPECIFIC CARD --> <!-- PRIMARY GROUPS CARDS -->
<b-card no-body v-if="userGroups"> <b-card
v-for="(group, name, index) in filteredGroups" :key="name"
no-body
>
<b-card-header class="d-flex align-items-center"> <b-card-header class="d-flex align-items-center">
<h2> <h2>
<icon iname="group" /> {{ $t('group_specific_permissions') }} <icon iname="group" /> {{ group.isSpecial ? $t('group_' + name) : `${$t('group')} "${name}"` }}
</h2> </h2>
<div class="ml-auto"> <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> <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
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> </div>
</b-card-header> </b-card-header>
<b-collapse id="collapse-specific" visible> <b-collapse :id="'collapse-' + index" visible>
<b-card-body> <b-card-body>
<div v-for="name in userGroupsNames" :key="name"> <b-row>
<b-row> <b-col md="3" lg="2">
<b-col md="3" lg="2"> <strong>{{ $t('users') }}</strong>
<icon iname="user" /> <strong>{{ name }}</strong> </b-col>
</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 <zone-selectize
item-icon="key-modern" item-variant="dark" :choices="group.availableMembers" :selected="group.members"
:choices="userGroups[name].availablePermissions" item-icon="user"
:selected="userGroups[name].permissions" :label="$t('group_add_member')"
:label="$t('group_add_permission')" @change="onUserChanged({ ...$event, name })"
:format="formatPermission"
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
/> />
</b-col> </template>
</b-row> </b-col>
<hr> </b-row>
</div> <hr>
<b-row>
<base-selectize <b-col md="3" lg="2">
v-if="availableMembers.length" <strong>{{ $t('permissions') }}</strong>
:label="$t('group_add_member')" </b-col>
:choices="availableMembers" <b-col>
:selected="userGroupsNames" <zone-selectize
@selected="onSpecificUserAdded" 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-card-body>
</b-collapse> </b-collapse>
</b-card> </b-card>
<!-- DELETE GROUP MODAL --> <!-- GROUP SPECIFIC CARD -->
<b-modal <template #extra>
v-if="groupToDelete" id="delete-modal" centered <b-card no-body v-if="userGroups">
body-bg-variant="danger" body-text-variant="light" <b-card-header class="d-flex align-items-center">
@ok="deleteGroup" hide-header <h2>
> <icon iname="group" /> {{ $t('group_specific_permissions') }}
{{ $t('confirm_delete', {name: groupToDelete }) }} </h2>
</b-modal>
</div> <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> </template>
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import api from '@/api' import api from '@/api'
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'
// TODO add global search with type (search by: group, user, permission) // TODO add global search with type (search by: group, user, permission)
// TODO add vuex store update on inputs ? // TODO add vuex store update on inputs ?
export default { export default {
name: 'GroupList', name: 'GroupList',
data: () => ({ data () {
search: '', return {
permissions: undefined, search: '',
normalGroups: undefined, permissions: undefined,
userGroups: undefined, normalGroups: undefined,
groupToDelete: undefined userGroups: undefined,
}), groupToDelete: undefined
}
},
computed: { computed: {
filteredGroups () { filteredGroups () {
@ -177,7 +178,7 @@ export default {
filtered[name] = groups[name] filtered[name] = groups[name]
} }
} }
return filtered return isEmptyValue(filtered) ? null : filtered
}, },
userGroupsNames () { userGroupsNames () {
@ -254,7 +255,7 @@ export default {
// updates while modifying values. // updates while modifying values.
const normalGroups = {} const normalGroups = {}
const userGroups = {} const userGroups = {}
const userNames = Object.keys(users) const userNames = users ? Object.keys(users) : []
for (const groupName in allGroups) { for (const groupName in allGroups) {
// copy the group to unlink it from the store // copy the group to unlink it from the store
@ -293,6 +294,7 @@ export default {
}, },
components: { components: {
SearchView,
ZoneSelectize, ZoneSelectize,
BaseSelectize BaseSelectize
} }

View file

@ -1,14 +1,11 @@
<template> <template>
<div class="service-list"> <search-view
<div class="actions"> id="service-list"
<b-input-group> :search.sync="search"
<b-input-group-prepend is-text> :items="services"
<icon iname="search" /> :filtered-items="filteredServices"
</b-input-group-prepend> items-name="services"
<b-form-input id="search-service" v-model="search" :placeholder="$t('search.service')" /> >
</b-input-group>
</div>
<b-list-group v-if="filteredServices"> <b-list-group v-if="filteredServices">
<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"
@ -17,7 +14,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="w-100"> <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"> <p class="mb-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'" />
@ -29,17 +29,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>
</div> </search-view>
</template> </template>
<script> <script>
import api from '@/api' 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: function () { data () {
return { return {
search: '', search: '',
services: undefined services: undefined
@ -50,16 +51,13 @@ export default {
filteredServices () { filteredServices () {
if (!this.services) return if (!this.services) return
const search = this.search.toLowerCase() const search = this.search.toLowerCase()
return 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
} }
}, },
filters: {
distanceToNow
},
methods: { methods: {
fetchData () { fetchData () {
// simply use the api helper since we will not store the request's result. // simply use the api helper since we will not store the request's result.
@ -77,6 +75,12 @@ export default {
created () { created () {
this.fetchData() this.fetchData()
},
components: { SearchView },
filters: {
distanceToNow
} }
} }
</script> </script>

View file

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

View file

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