@ -2,7 +2,9 @@
< w -page class = "admin-dashboard" >
< div class = "flex flex-wrap p-4 items-center" >
< div class = "flex-none" >
< img class = "admin-icon animated fadeInLeft" src = "/_assets/icons/fluent-apps-tab-animated.svg" / >
< img
class = "admin-icon animated fadeInLeft"
src = "/_assets/icons/fluent-apps-tab-animated.svg" / >
< / div >
< div class = "min-w-0 flex-1 pl-4" >
< div class = "text-h5 text-primary animated fadeInLeft" > { { t ( 'admin.dashboard.title' ) } } < / div >
@ -10,6 +12,28 @@
{ { t ( 'admin.dashboard.subtitle' ) } }
< / div >
< / div >
< div class = "flex-none flex" >
< w -btn
class = "mr-2 acrylic-btn"
icon = "la:question-circle"
flat
color = "grey"
: aria - label = "t(`common.actions.viewDocs`)"
: href = "siteStore.docsBase + `/admin`"
target = "_blank" >
< w -tooltip > { { t ( ` common.actions.viewDocs ` ) } } < / w - t o o l t i p >
< / w - b t n >
< w -btn
class = "mr-2 acrylic-btn"
icon = "la:redo-alt"
flat
color = "secondary"
: loading = "state.loading > 0"
: aria - label = "t(`common.actions.refresh`)"
@ click = "load" >
< w -tooltip > { { t ( ` common.actions.refresh ` ) } } < / w - t o o l t i p >
< / w - b t n >
< / div >
< / div >
< div class = "grid grid-cols-12 px-4 gap-2" >
< div class = "col-span-12 sm:col-span-6 lg:col-span-3" >
@ -57,7 +81,7 @@
: color = "actionColor"
icon = "la:plus-circle"
: label = "t(`common.actions.new`)"
: disable = "!userStore.can(`manage: user s`)"
: disable = "!userStore.can(`manage: group s`)"
@ click = "newGroup" / >
< w -separator vertical / >
< w -btn
@ -65,7 +89,7 @@
: color = "actionColor"
icon = "la:users"
: label = "t(`common.actions.manage`)"
: disable = "! userStore.can(`manage:users`) "
: disable = "! groupsAreVisible "
to = "/_admin/groups" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
@ -94,7 +118,7 @@
: color = "actionColor"
icon = "la:user-friends"
: label = "t(`common.actions.manage`)"
: disable = "!user Store.can(`manage:users`) "
: disable = "!user sAreVisible "
to = "/_admin/users" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
@ -137,27 +161,106 @@
< / w - c a r d - a c t i o n s >
< / w - c a r d >
< / div >
<!-- . col - 12. col - lg - 9 -- >
<!-- q - card -- >
<!-- q - card - section -- - -- >
< div class = "col-span-12" >
< w -banner
class = "bg-positive text-white"
: class = "adminStore.isVersionLatest ? `bg-positive` : `bg-warning`"
inline - actions >
< w -icon name = "la:check" class = "mr-2" / >
< span class = "font-medium" v -if = " adminStore.isVersionLatest "
> Your Wiki . js server is running the latest version ! < / s p a n
>
< span class = "font-medium" v -else
> A new version of Wiki . js is available . Please update to the latest version . < / s p a n
>
< template # action v-if ="userStore.can(`manage:system`)" >
< w -btn flat :label ="t(`admin.system.checkForUpdates`)" @click ="checkForUpdates" / >
< w -separator class = "mx-2" vertical dark / >
< w -btn flat :label ="t(`admin.system.title`)" to = "/_admin/system" / >
< / template >
< / w - b a n n e r >
< div class = "col-span-12 sm:col-span-6 lg:col-span-3" >
< w -card >
< w -card -section class = "admin-dashboard-card" >
< img :src ="versionCard.icon" / >
< div >
< strong > Wiki . js version < / strong >
< small : class = "{ pending: versionCard.pending }"
> { { versionCard . status } }
< i v -if = " versionCard.version "
> ( { { versionCard . version
} } < w -icon
v - if = "versionCard.latestVersion"
name = "mdi:arrow-right"
class = "mx-1 align-middle" / > { { versionCard . latestVersion } } ) < / i
> < / s m a l l
>
< / div >
< / w - c a r d - s e c t i o n >
< w -separator / >
< w -card -actions align = "right" >
< w -btn
flat
: color = "actionColor"
icon = "la:sync-alt"
: label = "t(`admin.system.checkForUpdates`)"
: disable = "!userStore.can(`manage:system`)"
@ click = "checkForUpdates" / >
< w -separator vertical / >
< w -btn
flat
: color = "actionColor"
icon = "la:info-circle"
: label = "t(`admin.system.title`)"
: disable = "!userStore.can(`manage:system`)"
to = "/_admin/system" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
< / div >
< div class = "col-span-12 sm:col-span-6 lg:col-span-3" >
< w -card >
< w -card -section class = "admin-dashboard-card" >
< img src = "/_assets/icons/fluent-bot.svg" / >
< div >
< strong > { { t ( 'admin.dashboard.activeWorkers' ) } } < / strong >
< span > { { adminStore . info . activeWorkers } } < / span >
< / div >
< / w - c a r d - s e c t i o n >
< w -separator / >
< w -card -actions align = "right" >
< w -btn
flat
: color = "actionColor"
icon = "la:tasks"
: label = "t(`admin.scheduler.title`)"
: disable = "!userStore.can(`manage:system`)"
to = "/_admin/scheduler" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
< / div >
< div class = "col-span-12 sm:col-span-6 lg:col-span-3" >
< w -card >
< w -card -section class = "admin-dashboard-card" >
< img src = "/_assets/icons/fluent-network.svg" / >
< div >
< strong > { { t ( 'admin.instances.title' ) } } < / strong >
< span > { { adminStore . info . instancesTotal } } < / span >
< / div >
< / w - c a r d - s e c t i o n >
< w -separator / >
< w -card -actions align = "right" >
< w -btn
flat
: color = "actionColor"
icon = "la:server"
: label = "t(`common.actions.view`)"
: disable = "!userStore.can(`manage:system`)"
to = "/_admin/instances" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
< / div >
< div class = "col-span-12 sm:col-span-6 lg:col-span-3" >
< w -card >
< w -card -section class = "admin-dashboard-card" >
< img src = "/_assets/icons/fluent-lightning-bolt.svg" / >
< div >
< strong > { { t ( 'admin.webhooks.title' ) } } < / strong >
< span > { { adminStore . info . webhooksTotal } } < / span >
< / div >
< / w - c a r d - s e c t i o n >
< w -separator / >
< w -card -actions align = "right" >
< w -btn
flat
: color = "actionColor"
icon = "la:bolt"
: label = "t(`common.actions.manage`)"
: disable = "!userStore.can(`manage:system`)"
to = "/_admin/webhooks" / >
< / w - c a r d - a c t i o n s >
< / w - c a r d >
< / div >
< / div >
< / w - p a g e >
@ -166,13 +269,15 @@
< script setup >
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import { computed , reactive } from 'vue'
import { useMeta } from '@/composables/meta'
import { dialog } from '@/composables/dialog'
import { useDark } from '@/composables/dark'
import { notify } from '@/composables/notify'
import { useFlagsStore } from '@/stores/flags'
import { useSiteStore } from '@/stores/site'
import { useUserStore } from '@/stores/user'
import { useAdminStore } from '../stores/admin'
@ -185,6 +290,7 @@ import GroupCreateDialog from '@/components/GroupCreateDialog.vue'
const adminStore = useAdminStore ( )
const flagsStore = useFlagsStore ( )
const siteStore = useSiteStore ( )
const userStore = useUserStore ( )
/ / C O M P O S A B L E S
@ -198,6 +304,15 @@ const dark = useDark()
* /
const actionColor = computed ( ( ) => ( dark . isActive ? 'primary-light' : 'primary' ) )
/ *
Manage only opens the list , which ` read:* ` is enough for -- the same rule the nav entries in
` AdminLayout ` use . Creating one is what needs ` manage:* ` .
* /
const groupsAreVisible = computed (
( ) => userStore . can ( 'read:groups' ) || userStore . can ( 'manage:groups' )
)
const usersAreVisible = computed ( ( ) => userStore . can ( 'read:users' ) || userStore . can ( 'manage:users' ) )
/ / R O U T E R
const router = useRouter ( )
@ -206,6 +321,43 @@ const router = useRouter()
const { t } = useI18n ( )
/ / D A T A
const state = reactive ( {
loading : 0
} )
/ / C O M P U T E D
const versionCard = computed ( ( ) => {
switch ( adminStore . versionStatus ) {
case 'latest' :
return {
icon : '/_assets/icons/fluent-done.svg' ,
status : t ( 'admin.dashboard.versionUpToDate' ) ,
version : adminStore . info . currentVersion ,
latestVersion : null ,
pending : false
}
case 'outdated' :
return {
icon : '/_assets/icons/fluent-double-up.svg' ,
status : t ( 'admin.dashboard.versionUpdateAvailable' ) ,
version : adminStore . info . currentVersion ,
latestVersion : adminStore . info . latestVersion ,
pending : false
}
default :
return {
icon : '/_assets/icons/fluent-refresh.svg' ,
status : t ( 'admin.dashboard.versionChecking' ) ,
version : null ,
latestVersion : null ,
pending : true
}
}
} )
/ / M E T A
useMeta ( {
@ -214,6 +366,25 @@ useMeta({
/ / M E T H O D S
/ *
Every card reads from the admin store , which ` AdminLayout ` fills once on mount -- ` fetchInfo ` for
the counters on ` info ` , ` fetchSites ` for the sites card , which counts the list itself . Refreshing
the dashboard is therefore both of them , not a call of its own .
* /
async function load ( ) {
state . loading ++
try {
await Promise . all ( [ adminStore . fetchInfo ( ) , adminStore . fetchSites ( ) ] )
} catch ( err ) {
notify ( {
type : 'negative' ,
message : 'Failed to refresh the dashboard.' ,
caption : err . message
} )
}
state . loading --
}
function newSite ( ) {
dialog ( {
component : SiteCreateDialog
@ -280,6 +451,18 @@ function checkForUpdates() {
font - size : 1 rem ;
font - style : normal ;
}
/ *
Amber itself ( # ffc107 ) is picked to read on the dark surface ; on the white card it lands
around 1.7 : 1 , so the light theme takes the darker end of the ramp instead .
* /
& . pending {
color : var ( -- color - amber - 9 ) ;
@ at - root . body -- dark & {
color : var ( -- color - amber ) ;
}
}
}
}