refactor: rework async GroupList

This commit is contained in:
axolotle 2024-08-13 00:26:49 +02:00
parent 3e44d959eb
commit fe380005a5

View file

@ -1,165 +1,151 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import api from '@/api' import api from '@/api'
import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem.vue' import TagsSelectizeItem from '@/components/globals/formItems/TagsSelectizeItem.vue'
import { useAutoModal } from '@/composables/useAutoModal' import { useAutoModal } from '@/composables/useAutoModal'
import { useInitialQueries } from '@/composables/useInitialQueries'
import { useSearch } from '@/composables/useSearch' import { useSearch } from '@/composables/useSearch'
import { isEmptyValue } from '@/helpers/commons' import { toEntries } from '@/helpers/commons'
import type { Obj } from '@/types/commons' import type { Obj } from '@/types/commons'
import type { Group, Permission, UserItem } from '@/types/core/data'
import type { TagUpdateArgs } from '@/types/form'
// 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 ?
const { t } = useI18n() const { t } = useI18n()
const modalConfirm = useAutoModal() const modalConfirm = useAutoModal()
const { loading } = useInitialQueries(
[ const {
primaryGroups,
userGroups,
permissionOptions,
userOptions,
activeUserGroups,
} = await api
.fetchAll<[Obj<UserItem>, Obj<Group>, Obj<Permission>]>([
{ uri: 'users', cachePath: 'users' }, { uri: 'users', cachePath: 'users' },
{ {
uri: 'users/groups?full&include_primary_groups', uri: 'users/groups?full&include_primary_groups',
cachePath: 'groups', cachePath: 'groups',
}, },
{ uri: 'users/permissions?full', cachePath: 'permissions' }, { uri: 'users/permissions?full', cachePath: 'permissions' },
], ])
{ onQueriesResponse }, .then(([users, groups, permsDict]) => {
type DGroup = Group & {
members: string[]
name: string
isSpecial?: boolean
disabledItems?: string[]
}
const permIds = Object.keys(permsDict)
const userNames = users ? Object.keys(users) : []
const specialGroupsFilters = {
visitors: (id: string) => permsDict[id].protected,
all_users: (id: string) => ['ssh.main', 'sftp.main'].includes(id),
admins: (id: string) => ['ssh.main', 'sftp.main'].includes(id),
}
function isSpecialGroup(
name: string,
): name is 'visitors' | 'all_users' | 'admins' {
return ['visitors', 'all_users', 'admins'].includes(name)
}
const { primaryGroups, userGroups } = toEntries(groups).reduce(
(g, [name, data]) => {
const group: DGroup = {
name,
// Clone data to avoid mutating the cache
members: [...data.members],
permissions: [...data.permissions],
}
if (userNames.includes(name)) {
g.userGroups[name] = group
} else {
if (isSpecialGroup(name)) {
group.isSpecial = true
// Forbid to add or remove a protected permission on group `visitors`
// Forbid to add ssh and sftp permission on group `all_users` and `admins`
group.disabledItems = permIds.filter(specialGroupsFilters[name])
}
g.primaryGroups.push(group)
}
return g
},
{ primaryGroups: [] as DGroup[], userGroups: {} as Obj<DGroup> },
) )
const permissions = ref() return {
const permissionsOptions = ref() primaryGroups: ref(primaryGroups),
const primaryGroups = ref<Obj[] | undefined>() userGroups: reactive(userGroups),
const userGroups = ref() permissionOptions: permIds.map((id) => ({
const usersOptions = ref() value: id,
const activeUserGroups = ref() text: permsDict[id].label,
})),
userOptions: userNames,
activeUserGroups: ref(
Object.values(userGroups)
.filter((group) => group.permissions.length > 0)
.map((group) => group.name),
),
}
})
const [search, filteredGroups] = useSearch(primaryGroups, (s, group) => { const [search, filteredGroups] = useSearch(primaryGroups, (s, group) => {
return group.name.toLowerCase().includes(s) return group.name.toLowerCase().includes(s)
}) })
function onQueriesResponse(users: any, allGroups: any, permsDict: any) { async function onPermissionChanged(
// Do not use computed properties to get values from the store here to avoid auto { tag: perm, action, applyFn }: TagUpdateArgs,
// updates while modifying values. name: string,
const permissions_ = Object.entries(permsDict).map(([id, value]) => ({ ) {
id, if (action === 'add' && ['sftp.main', 'ssh.main'].includes(perm)) {
...value,
}))
const userNames = users ? Object.keys(users) : []
const primaryGroups_ = []
const userGroups_ = {}
for (const groupName in allGroups) {
// copy the group to unlink it from the store
const group_ = { ...allGroups[groupName], name: groupName }
group_.permissions = group_.permissions.map((perm) => {
return permsDict[perm].label
})
if (userNames.includes(groupName)) {
userGroups_[groupName] = group_
continue
}
group_.isSpecial = ['visitors', 'all_users', 'admins'].includes(groupName)
if (groupName === 'visitors') {
// Forbid to add or remove a protected permission on group `visitors`
group_.disabledItems = permissions_
.filter(({ id }) => {
return (
['mail.main', 'xmpp.main'].includes(id) || permsDict[id].protected
)
})
.map(({ id }) => permsDict[id].label)
}
if (groupName === 'all_users') {
// Forbid to add ssh and sftp permission on group `all_users`
group_.disabledItems = permissions_
.filter(({ id }) => {
return ['ssh.main', 'sftp.main'].includes(id)
})
.map(({ id }) => permsDict[id].label)
}
if (groupName === 'admins') {
// Forbid to add ssh and sftp permission on group `admins`
group_.disabledItems = permissions_
.filter(({ id }) => {
return ['ssh.main', 'sftp.main'].includes(id)
})
.map(({ id }) => permsDict[id].label)
}
primaryGroups_.push(group_)
}
const activeUserGroups_ = Object.entries(userGroups_)
.filter(([_, group]) => {
return group.permissions.length > 0
})
.map(([name]) => name)
permissions.value = permissions_
permissionsOptions.value = permissions_.map((perm) => perm.label)
primaryGroups.value = primaryGroups_
userGroups.value = isEmptyValue(userGroups_) ? null : userGroups_
usersOptions.value = userNames
activeUserGroups.value = activeUserGroups_
}
async function onPermissionChanged({ option, groupName, action, applyMethod }) {
const permId = permissions.value.find((perm) => perm.label === option).id
if (action === 'add' && ['sftp.main', 'ssh.main'].includes(permId)) {
const confirmed = await modalConfirm( const confirmed = await modalConfirm(
t('confirm_group_add_access_permission', { t('confirm_group_add_access_permission', { name, perm }),
name: groupName,
perm: option,
}),
) )
if (!confirmed) return if (!confirmed) return
} }
api api
.put({ .put({
uri: `users/permissions/${permId}/${action}/${groupName}`, uri: `users/permissions/${perm}/${action}/${name}`,
cachePath: `permissions.${permId}`, cachePath: `permissions.${perm}`,
humanKey: { key: 'permissions.' + action, perm: option, name: groupName }, humanKey: { key: `permissions.${action}`, perm, name },
}) })
.then(() => applyMethod(option)) .then(() => applyFn(perm))
} }
function onUserChanged({ option, groupName, action, applyMethod }) { function onUserChanged(
{ tag: user, action, applyFn }: TagUpdateArgs,
name: string,
) {
api api
.put({ .put({
uri: `users/groups/${groupName}/${action}/${option}`, uri: `users/groups/${name}/${action}/${user}`,
cachePath: `groups.${groupName}`, cachePath: `groups.${name}`,
humanKey: { key: 'groups.' + action, user: option, name: groupName }, humanKey: { key: `groups.${action}`, user, name },
}) })
.then(() => applyMethod(option)) .then(() => applyFn(user))
} }
function onSpecificUserAdded({ option: userName, action, applyMethod }) { async function deleteGroup(name: string) {
if (action === 'add') { const confirmed = await modalConfirm(t('confirm_delete', { name }))
userGroups.value[userName].permissions = []
applyMethod(userName)
}
}
async function deleteGroup(groupName) {
const confirmed = await modalConfirm(t('confirm_delete', { name: groupName }))
if (!confirmed) return if (!confirmed) return
api api
.delete({ .delete({
uri: `users/groups/${groupName}`, uri: `users/groups/${name}`,
cachePath: `groups.${groupName}`, cachePath: `groups.${name}`,
humanKey: { key: 'groups.delete', name: groupName }, humanKey: { key: 'groups.delete', name },
}) })
.then(() => { .then(() => {
primaryGroups.value = primaryGroups.value?.filter( // FIXME primaryGroups as ref to override it ?
(group) => group.name !== groupName, primaryGroups.value = primaryGroups.value.filter(
(group) => group.name !== name,
) )
}) })
} }
@ -170,7 +156,6 @@ async function deleteGroup(groupName) {
v-model="search" v-model="search"
:items="filteredGroups" :items="filteredGroups"
items-name="groups" items-name="groups"
:loading="loading"
skeleton="CardFormSkeleton" skeleton="CardFormSkeleton"
> >
<template #top-bar-buttons> <template #top-bar-buttons>
@ -222,12 +207,12 @@ async function deleteGroup(groupName) {
<template v-if="group.name == 'admins' || !group.isSpecial"> <template v-if="group.name == 'admins' || !group.isSpecial">
<TagsSelectizeItem <TagsSelectizeItem
v-model="group.members" v-model="group.members"
:options="usersOptions" :options="userOptions"
:id="group.name + '-users'" :id="group.name + '-users'"
:label="$t('group_add_member')" :label="$t('group_add_member')"
tag-icon="user" tag-icon="user"
items-name="users" items-name="users"
@tag-update="onUserChanged({ ...$event, groupName: group.name })" @tag-update="onUserChanged($event, group.name)"
/> />
</template> </template>
</BCol> </BCol>
@ -241,27 +226,20 @@ async function deleteGroup(groupName) {
<BCol> <BCol>
<TagsSelectizeItem <TagsSelectizeItem
v-model="group.permissions" v-model="group.permissions"
:options="permissionsOptions" :options="permissionOptions"
:id="group.name + '-perms'" :id="group.name + '-perms'"
:label="$t('group_add_permission')" :label="$t('group_add_permission')"
tag-icon="key-modern" tag-icon="key-modern"
items-name="permissions" items-name="permissions"
@tag-update="
onPermissionChanged({ ...$event, groupName: group.name })
"
:disabled-items="group.disabledItems" :disabled-items="group.disabledItems"
@tag-update="onPermissionChanged($event, group.name)"
/> />
</BCol> </BCol>
</BRow> </BRow>
</YCard> </YCard>
<!-- USER GROUPS CARD --> <!-- USER GROUPS CARD -->
<YCard <YCard collapsable :title="$t('group_specific_permissions')" icon="group">
v-if="userGroups"
collapsable
:title="$t('group_specific_permissions')"
icon="group"
>
<template v-for="userName in activeUserGroups" :key="userName"> <template v-for="userName in activeUserGroups" :key="userName">
<BRow> <BRow>
<BCol md="3" lg="2"> <BCol md="3" lg="2">
@ -271,14 +249,12 @@ async function deleteGroup(groupName) {
<BCol> <BCol>
<TagsSelectizeItem <TagsSelectizeItem
v-model="userGroups[userName].permissions" v-model="userGroups[userName].permissions"
:options="permissionsOptions" :options="permissionOptions"
:id="userName + '-perms'" :id="userName + '-perms'"
:label="$t('group_add_permission')" :label="$t('group_add_permission')"
tag-icon="key-modern" tag-icon="key-modern"
items-name="permissions" items-name="permissions"
@tag-update=" @tag-update="onPermissionChanged($event, userName)"
onPermissionChanged({ ...$event, groupName: userName })
"
/> />
</BCol> </BCol>
</BRow> </BRow>
@ -287,12 +263,12 @@ async function deleteGroup(groupName) {
<TagsSelectizeItem <TagsSelectizeItem
v-model="activeUserGroups" v-model="activeUserGroups"
:options="usersOptions" auto
:options="userOptions"
id="user-groups" id="user-groups"
:label="$t('group_add_member')" :label="$t('group_add_member')"
no-tags no-tags
items-name="users" items-name="users"
@tag-update="onSpecificUserAdded"
/> />
</YCard> </YCard>
</ViewSearch> </ViewSearch>