<template lang="pug"> q-layout(view='hHh lpR fFf', container) q-header.card-header.q-px-md.q-py-sm q-icon(name='img:/_assets/icons/fluent-people.svg', left, size='md') div span {{t(`admin.groups.edit`)}} .text-caption {{state.group.name}} q-space q-btn-group(push) q-btn( push color='grey-6' text-color='white' :aria-label='t(`common.actions.refresh`)' icon='las la-redo-alt' @click='refresh' ) q-tooltip(anchor='center left', self='center right') {{t(`common.actions.refresh`)}} q-btn( push color='white' text-color='grey-7' :label='t(`common.actions.close`)' icon='las la-times' @click='close' ) q-btn( push color='positive' text-color='white' :label='t(`common.actions.save`)' icon='las la-check' ) q-drawer.bg-dark-6(:model-value='true', :width='250', dark) q-list(padding, v-show='!state.isLoading') q-item( v-for='sc of sections' :key='`section-` + sc.key' clickable :to='{ params: { section: sc.key } }' active-class='bg-primary text-white' :disabled='sc.disabled' ) q-item-section(side) q-icon(:name='sc.icon', color='white') q-item-section {{sc.text}} q-item-section(side, v-if='sc.usersTotal') q-badge(color='dark-3', :label='state.usersTotal') q-item-section(side, v-if='sc.rulesTotal && state.group.rules') q-badge(color='dark-3', :label='state.group.rules.length') q-page-container q-page(v-if='state.isLoading') //- ----------------------------------------------------------------------- //- OVERVIEW //- ----------------------------------------------------------------------- q-page(v-else-if='route.params.section === `overview`') .q-pa-md .row.q-col-gutter-md .col-12.col-lg-8 q-card.shadow-1.q-pb-sm q-card-section .text-subtitle1 {{t('admin.groups.general')}} q-item blueprint-icon(icon='team') q-item-section q-item-label {{t(`admin.groups.name`)}} q-item-label(caption) {{t(`admin.groups.nameHint`)}} q-item-section q-input( outlined v-model='state.group.name' dense :rules='groupNameValidation' hide-bottom-space :aria-label='t(`admin.groups.name`)' ) q-card.shadow-1.q-pb-sm.q-mt-md q-card-section .text-subtitle1 {{t('admin.groups.authBehaviors')}} q-item blueprint-icon(icon='double-right') q-item-section q-item-label {{t(`admin.groups.redirectOnLogin`)}} q-item-label(caption) {{t(`admin.groups.redirectOnLoginHint`)}} q-item-section q-input( outlined v-model='state.group.redirectOnLogin' dense :aria-label='t(`admin.groups.redirectOnLogin`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='chevron-right') q-item-section q-item-label {{t(`admin.groups.redirectOnFirstLogin`)}} q-item-label(caption) {{t(`admin.groups.redirectOnFirstLoginHint`)}} q-item-section q-input( outlined v-model='state.group.redirectOnFirstLogin' dense :aria-label='t(`admin.groups.redirectOnLogin`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='exit') q-item-section q-item-label {{t(`admin.groups.redirectOnLogout`)}} q-item-label(caption) {{t(`admin.groups.redirectOnLogoutHint`)}} q-item-section q-input( outlined v-model='state.group.redirectOnLogout' dense :aria-label='t(`admin.groups.redirectOnLogout`)' ) .col-12.col-lg-4 q-card.shadow-1.q-pb-sm q-card-section .text-subtitle1 {{t('admin.groups.info')}} q-item blueprint-icon(icon='team', :hue-rotate='-45') q-item-section q-item-label {{t(`common.field.id`)}} q-item-label: strong {{state.group.id}} q-separator.q-my-sm(inset) q-item blueprint-icon(icon='calendar-plus', :hue-rotate='-45') q-item-section q-item-label {{t(`common.field.createdOn`)}} q-item-label: strong {{humanizeDate(state.group.createdAt)}} q-separator.q-my-sm(inset) q-item blueprint-icon(icon='summertime', :hue-rotate='-45') q-item-section q-item-label {{t(`common.field.lastUpdated`)}} q-item-label: strong {{humanizeDate(state.group.updatedAt)}} //- ----------------------------------------------------------------------- //- RULES //- ----------------------------------------------------------------------- q-page(v-else-if='route.params.section === `rules`') q-toolbar.q-pl-md( :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' ) .text-subtitle1 {{t('admin.groups.rules')}} q-space q-btn.acrylic-btn.q-mr-sm( icon='las la-question-circle' flat color='grey' type='a' href='https://docs.js.wiki/admin/groups#rules' target='_blank' ) q-btn.acrylic-btn.q-mr-sm( flat color='indigo' icon='las la-file-export' @click='exportRules' ) q-tooltip {{t('admin.groups.exportRules')}} q-btn.acrylic-btn.q-mr-sm( flat color='indigo' icon='las la-file-import' @click='importRules' ) q-tooltip {{t('admin.groups.importRules')}} q-btn( unelevated color='primary' icon='las la-plus' label='New Rule' @click='newRule' ) q-separator .q-pa-md q-banner( v-if='!state.group.rules || state.group.rules.length < 1' rounded :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`' ) {{t('admin.groups.rulesNone')}} q-card.shadow-1.q-pb-sm(v-else) q-card-section .admin-groups-rule( v-for='(rule, idx) of state.group.rules' :key='rule.id' ) .admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)') q-icon.cursor-pointer( :name='getRuleModeIcon(rule.mode)' color='white' @click='rule.mode = getNextRuleMode(rule.mode)' ) .admin-groups-rule-name .admin-groups-rule-name-text: strong(:class='getRuleModeColor(rule.mode)') {{getRuleModeName(rule.mode)}} q-separator.q-ml-sm.q-mr-xs(vertical) input( type='text' v-model='rule.name' placeholder='Rule Name' ) q-card.admin-groups-rule-card.q-mt-md(flat) q-card-section.admin-groups-rule-card-permissions(:class='getRuleModeClass(rule.mode)') q-select.q-mt-xs( standout v-model='rule.roles' emit-value map-options dense :aria-label='t(`admin.groups.ruleSites`)' :options='rules' placeholder='Select permissions...' option-value='permission' option-label='title' options-dense multiple use-chips stack-label ) template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }') q-item(v-bind='itemProps', v-on='itemEvents') q-item-section(side) q-toggle( :value='selected' @input='toggleOption(opt)' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='opt.label' ) //- q-item-section(side, style='flex-basis: 150px;') //- q-chip.text-caption( //- square //- color='teal' //- text-color='white' //- dense //- ) {{opt.permission}} q-item-section q-item-label {{opt.title}} q-item-label(caption) {{opt.hint}} q-btn.acrylic-btn.q-ml-md( flat icon='las la-trash' color='negative' padding='sm sm' size='md', @click='deleteRule(rule.id)' ) q-card-section(horizontal) q-card-section.admin-groups-rule-card-filters .text-caption Applies to... q-select.q-mt-xs( standout v-model='rule.sites' emit-value map-options dense :aria-label='t(`admin.groups.ruleSites`)' :options='adminStore.sites' option-value='id' option-label='title' multiple behavior='dialog' :display-value='t(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })' ) template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }') q-item(v-bind='itemProps', v-on='itemEvents') q-item-section q-item-label {{opt.title}} q-item-section(side) q-toggle( :value='selected' @input='toggleOption(opt)' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='opt.label' ) q-select.q-mt-sm( standout v-model='rule.locales' emit-value map-options dense :aria-label='t(`admin.groups.ruleLocales`)' :options='adminStore.locales' option-value='code' option-label='name' multiple behavior='dialog' :display-value='t(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })' ) template(v-slot:option='{ itemProps, opt, selected, toggleOption }') q-item(v-bind='itemProps') q-item-section q-item-label {{opt.name}} q-item-section(side) q-toggle( :model-value='selected' @update:model-value='toggleOption(opt)' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='opt.name' ) q-card-section.admin-groups-rule-card-pattern .text-caption Pattern q-select.q-mt-xs( standout v-model='rule.match' emit-value map-options dense :aria-label='t(`admin.groups.ruleMatch`)' :options=`[ { label: t('admin.groups.ruleMatchStart'), value: 'START' }, { label: t('admin.groups.ruleMatchEnd'), value: 'END' }, { label: t('admin.groups.ruleMatchRegex'), value: 'REGEX' }, { label: t('admin.groups.ruleMatchTag'), value: 'TAG' }, { label: t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' }, { label: t('admin.groups.ruleMatchExact'), value: 'EXACT' } ]` ) q-input.q-mt-sm( standout v-model='rule.path' dense :prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null' :suffix='rule.match === `REGEX` ? `/` : null' :aria-label='t(`admin.groups.rulePath`)' ) //- ----------------------------------------------------------------------- //- PERMISSIONS //- ----------------------------------------------------------------------- q-page(v-else-if='route.params.section === `permissions`') .q-pa-md .row.q-col-gutter-md .col-12.col-lg-6 q-card.shadow-1.q-pb-sm .flex.justify-between q-card-section .text-subtitle1 {{t(`admin.groups.permissions`)}} q-card-section q-btn.acrylic-btn( icon='las la-question-circle' flat color='grey' type='a' href='https://docs.js.wiki/admin/groups#permissions' target='_blank' ) template(v-for='(perm, idx) of permissions', :key='perm.permission') q-item(tag='label', v-ripple) q-item-section.items-center(style='flex: 0 0 40px;') q-icon( name='las la-comments' color='primary' size='sm' ) q-item-section q-item-label {{perm.permission}} q-item-label(caption) {{perm.hint}} q-item-section(avatar) q-toggle( v-model='state.group.permissions' :val='perm.permission' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' :aria-label='t(`admin.general.allowComments`)' ) q-separator.q-my-sm(inset, v-if='idx < permissions.length - 1') //- ----------------------------------------------------------------------- //- USERS //- ----------------------------------------------------------------------- q-page(v-else-if='route.params.section === `users`') q-toolbar( :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' ) .text-subtitle1 {{t('admin.groups.users')}} q-space q-btn.acrylic-btn.q-mr-sm( icon='las la-question-circle' flat color='grey' type='a' href='https://docs.js.wiki/admin/groups#users' target='_blank' ) q-input.denser.fill-outline.q-mr-sm( outlined v-model='state.usersFilter' :placeholder='t(`admin.groups.filterUsers`)' dense ) template(#prepend) q-icon(name='las la-search') q-btn.q-mr-sm.acrylic-btn( icon='las la-redo-alt' flat color='secondary' @click='refreshUsers' ) q-btn.q-mr-xs( unelevated icon='las la-user-plus' :label='t(`admin.groups.assignUser`)' color='primary' @click='assignUser' ) q-separator .q-pa-md q-banner( v-if='!state.users || state.users.length < 1' rounded :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`' ) {{t('admin.groups.usersNone')}} q-card.shadow-1 q-table( :rows='state.users' :columns='usersHeaders' row-key='id' flat hide-header hide-bottom :rows-per-page-options='[0]' :loading='state.isLoadingUsers' ) template(v-slot:body-cell-id='props') q-td(:props='props') q-icon(name='las la-user', color='primary', size='sm') template(v-slot:body-cell-name='props') q-td(:props='props') .flex.items-center strong {{props.value}} q-icon.q-ml-sm( v-if='props.row.isSystem' name='las la-lock' color='pink' ) q-icon.q-ml-sm( v-if='!props.row.isActive' name='las la-ban' color='pink' ) template(v-slot:body-cell-email='props') q-td(:props='props') em {{ props.value }} template(v-slot:body-cell-date='props') q-td(:props='props') i18n-t.text-caption(keypath='admin.users.createdAt', tag='div') template(#date) strong {{ humanizeDate(props.value) }} i18n-t.text-caption( v-if='props.row.lastLoginAt' keypath='admin.users.lastLoginAt' tag='div' ) template(#date) strong {{ humanizeDate(props.row.lastLoginAt) }} template(v-slot:body-cell-edit='props') q-td(:props='props') q-btn.acrylic-btn.q-mr-sm( v-if='!props.row.isSystem' flat :to='`/_admin/users/` + props.row.id' icon='las la-pen' color='indigo' :label='t(`common.actions.edit`)' no-caps ) q-btn.acrylic-btn( v-if='!props.row.isSystem' flat icon='las la-user-minus' color='accent' @click='unassignUser(props.row)' ) .flex.flex-center.q-mt-md(v-if='usersTotalPages > 1') q-pagination( v-model='state.usersPage' :max='usersTotalPages' :max-pages='9' boundary-numbers direction-links ) </template> <script setup> import gql from 'graphql-tag' import { DateTime } from 'luxon' import { v4 as uuid } from 'uuid' import { cloneDeep, some } from 'lodash-es' import { fileOpen } from 'browser-fs-access' import { useI18n } from 'vue-i18n' import { exportFile, useQuasar } from 'quasar' import { computed, onMounted, reactive, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useAdminStore } from 'src/stores/admin' // QUASAR const $q = useQuasar() // STORES const adminStore = useAdminStore() // ROUTER const router = useRouter() const route = useRoute() // I18N const { t } = useI18n() // DATA const state = reactive({ group: { rules: [] }, isLoading: false, users: [], isLoadingUsers: false, usersFilter: '', usersPage: 1, usersPageSize: 15, usersTotal: 0 }) const sections = [ { key: 'overview', text: t('admin.groups.overview'), icon: 'las la-users' }, { key: 'rules', text: t('admin.groups.rules'), icon: 'las la-file-invoice', rulesTotal: true }, { key: 'permissions', text: t('admin.groups.permissions'), icon: 'las la-list-alt' }, { key: 'users', text: t('admin.groups.users'), icon: 'las la-user', usersTotal: true } ] const usersHeaders = [ { align: 'center', field: 'id', name: 'id', sortable: false, style: 'width: 20px' }, { label: t('common.field.name'), align: 'left', field: 'name', name: 'name', sortable: true }, { label: t('admin.users.email'), align: 'left', field: 'email', name: 'email', sortable: false }, { align: 'left', field: 'createdAt', name: 'date', sortable: false }, { label: '', align: 'right', field: 'edit', name: 'edit', sortable: false, style: 'width: 250px' } ] const permissions = [ { permission: 'write:users', hint: 'Can create or authorize new users, but not modify existing ones', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:users', hint: 'Can manage all users (but not users with administrative permissions)', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'write:groups', hint: 'Can manage groups and assign CONTENT permissions / page rules', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:groups', hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules', warning: true, restrictedForSystem: true, disabled: false }, { permission: 'manage:navigation', hint: 'Can manage the site navigation', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:theme', hint: 'Can manage and modify themes', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:api', hint: 'Can generate and revoke API keys', warning: true, restrictedForSystem: true, disabled: false }, { permission: 'manage:system', hint: 'Can manage and access everything. Root administrator.', warning: true, restrictedForSystem: true, disabled: true } ] const rules = [ { permission: 'read:pages', title: 'Read Pages', hint: 'Can view and search pages.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'write:pages', title: 'Write Pages', hint: 'Can create and edit pages.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'review:pages', title: 'Review Pages', hint: 'Can review and approve edits submitted by users.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:pages', title: 'Manage Pages', hint: 'Can move existing pages to other locations the user has write access to.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'delete:pages', title: 'Delete Pages', hint: 'Can delete existing pages.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'write:styles', title: 'Use CSS', hint: 'Can insert CSS styles in pages.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'write:scripts', title: 'Use JavaScript', hint: 'Can insert JavaScript in pages.', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'read:source', title: 'View Pages Source', hint: 'Can view pages source.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'read:history', title: 'View Page History', hint: 'Can view previous versions of pages.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'read:assets', title: 'View Assets', hint: 'Can view / use assets (such as images and files) in pages.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'write:assets', title: 'Upload Assets', hint: 'Can upload new assets (such as images and files).', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'manage:assets', title: 'Manage Assets', hint: 'Can edit and delete existing assets (such as images and files).', warning: false, restrictedForSystem: true, disabled: false }, { permission: 'read:comments', title: 'Read Comments', hint: 'Can view page comments.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'write:comments', title: 'Write Comments', hint: 'Can post new comments on pages.', warning: false, restrictedForSystem: false, disabled: false }, { permission: 'manage:comments', title: 'Manage Comments', hint: 'Can edit and delete existing page comments.', warning: false, restrictedForSystem: true, disabled: false } ] // VALIDATION RULES const groupNameValidation = [ val => /^[^<>"]+$/.test(val) || t('admin.groups.nameInvalidChars') ] // COMPUTED const usersTotalPages = computed(() => { if (state.usersTotal < 1) { return 0 } return Math.ceil(state.usersTotal / state.usersPageSize) }) // WATCHERS watch(() => route.params.section, checkRoute) watch([() => state.usersPage, () => state.usersFilter], refreshUsers) // METHODS function close () { adminStore.$patch({ overlay: '' }) } function checkRoute () { if (!route.params.section) { router.replace({ params: { section: 'overview' } }) } else if (route.params.section === 'users') { refreshUsers() } } function humanizeDate (val) { if (!val) { return '---' } return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_FULL) } function getRuleModeColor (mode) { return ({ DENY: 'text-negative', ALLOW: 'text-positive', FORCEALLOW: 'text-blue' })[mode] } function getRuleModeClass (mode) { return 'is-' + mode.toLowerCase() } function getRuleModeIcon (mode) { return ({ DENY: 'las la-ban', ALLOW: 'las la-check', FORCEALLOW: 'las la-check-double' })[mode] || 'las la-frog' } function getNextRuleMode (mode) { return ({ DENY: 'FORCEALLOW', ALLOW: 'DENY', FORCEALLOW: 'ALLOW' })[mode] || 'ALLOW' } function getRuleModeName (mode) { switch (mode) { case 'ALLOW': return t('admin.groups.ruleAllow') case 'DENY': return t('admin.groups.ruleDeny') case 'FORCEALLOW': return t('admin.groups.ruleForceAllow') default: return '???' } } function refresh () { fetchGroup() } async function fetchGroup () { state.isLoading = true try { const resp = await APOLLO_CLIENT.query({ query: gql` query adminFetchGroup ( $id: UUID! ) { groupById( id: $id ) { id name redirectOnLogin redirectOnFirstLogin redirectOnLogout isSystem permissions rules { id name path roles match mode locales sites } userCount createdAt updatedAt } } `, variables: { id: adminStore.overlayOpts.id }, fetchPolicy: 'network-only' }) if (resp?.data?.groupById) { state.group = cloneDeep(resp.data.groupById) state.usersTotal = state.group.userCount ?? 0 } else { throw new Error('An unexpected error occured while fetching group details.') } } catch (err) { $q.notify({ type: 'negative', message: err.message }) } state.isLoading = false } function newRule () { state.group.rules.push({ id: uuid(), name: t('admin.groups.ruleUntitled'), mode: 'ALLOW', match: 'START', roles: [], path: '', locales: [], sites: [] }) } function deleteRule (id) { state.group.rules = state.group.rules.filter(r => r.id !== id) } function exportRules () { if (state.group.rules.length < 1) { return $q.notify({ type: 'negative', message: t('admin.groups.exportRulesNoneError') }) } exportFile('rules.json', JSON.stringify(state.group.rules, null, 2), { mimeType: 'application/json;charset=UTF-8' }) } async function importRules () { try { const blob = await fileOpen({ mimeTypes: ['application/json'], extensions: ['.json'], startIn: 'downloads', excludeAcceptAllOption: true }) const rulesRaw = await blob.text() const rules = JSON.parse(rulesRaw) if (!Array.isArray(rules) || rules.length < 1) { throw new Error('Invalid Rules Format') } $q.dialog({ title: t('admin.groups.importModeTitle'), message: t('admin.groups.importModeText'), options: { model: 'replace', type: 'radio', items: [ { label: t('admin.groups.importModeReplace'), value: 'replace' }, { label: t('admin.groups.importModeAdd'), value: 'add' } ] }, persistent: true }).onOk(choice => { if (choice === 'replace') { state.group.rules = [] } state.group.rules = [ ...state.group.rules, ...rules.map(r => ({ id: uuid(), name: r.name || t('admin.groups.ruleUntitled'), mode: ['ALLOW', 'DENY', 'FORCEALLOW'].includes(r.mode) ? r.mode : 'DENY', match: ['START', 'END', 'REGEX', 'TAG', 'TAGALL', 'EXACT'].includes(r.match) ? r.match : 'START', roles: r.roles || [], path: r.path || '', locales: r.locales.filter(l => some(adminStore.locales, ['code', l])), sites: r.sites.filter(s => some(adminStore.sites, ['id', s])) })) ] $q.notify({ type: 'positive', message: t('admin.groups.importSuccess') }) }) } catch (err) { $q.notify({ type: 'negative', message: t('admin.groups.importFailed') + ` [${err.message}]` }) } } async function refreshUsers () { state.isLoadingUsers = true try { const resp = await APOLLO_CLIENT.query({ query: gql` query adminFetchGroupUsers ( $filter: String $page: Int $pageSize: Int $groupId: UUID! ) { groupById ( id: $groupId ) { id userCount users ( filter: $filter page: $page pageSize: $pageSize ) { id name email isSystem isActive createdAt lastLoginAt } } } `, variables: { filter: state.usersFilter, page: state.usersPage, pageSize: state.usersPageSize, groupId: adminStore.overlayOpts.id }, fetchPolicy: 'network-only' }) if (resp?.data?.groupById?.users) { state.usersTotal = resp.data.groupById.userCount ?? 0 state.users = cloneDeep(resp.data.groupById.users) } else { throw new Error('An unexpected error occured while fetching group users.') } } catch (err) { $q.notify({ type: 'negative', message: err.message }) } state.isLoadingUsers = false } function assignUser () { } function unassignUser () { } // MOUNTED onMounted(() => { checkRoute() fetchGroup() }) </script> <style lang="scss"> .admin-groups-rule { position: relative; padding: 10px 0 24px 40px; &-icon { position: absolute; top: 0; left: 0; bottom: 0; width: 31px; &::before { position: absolute; content: ""; border-radius: 100%; width: 31px; height: 31px; background-color: currentColor; top: 4px; } &::after { position: absolute; content: ""; width: 3px; top: 41px; bottom: 0; left: 14px; opacity: .4; background-color: currentColor; display: block; } .q-icon { position: absolute; top: 0; left: 0; right: 0; font-size: 16px; height: 38px; line-height: 38px; width: 100%; align-items: center; justify-content: center; display: flex; } } &-name { line-height: 12px; display: flex; flex-wrap: nowrap; padding-top: 4px; &-text { flex: 0 0; white-space: nowrap; } input { font-weight: 700; color: $grey-6; letter-spacing: 1px; font-size: 12px; line-height: 12px; border: none; padding: 0 0 0 5px; outline: none; flex: 1; background-color: transparent; &::placeholder { color: $grey-5; } @at-root .body--dark & { color: rgba(255,255,255,.7); &::placeholder { color: rgba(255,255,255,.4); } } } } &-card { background-color: $grey-2 !important; @at-root .body--dark & { background-color: $dark-6 !important; } &-permissions { background-color: rgba($positive, .1); border-bottom: 1px solid rgba($positive, .3); display: flex; align-items: center; .q-select { flex-basis: 100%; } &.is-allow { background-color: rgba($positive, .1); border-bottom: 1px solid rgba($positive, .3); } &.is-deny { background-color: rgba($negative, .1); border-bottom: 1px solid rgba($negative, .3); } &.is-forceallow { background-color: rgba($blue, .1); border-bottom: 1px solid rgba($blue, .3); } } &-filters { background-color: $grey-3; flex-basis: 300px; .text-caption:first-child { color: $grey-7; } @at-root .body--dark & { background-color: $dark-5; } } &-pattern { flex-grow: 1; } } } </style>