Bläddra i källkod

feat: admin storage actions + status light component

Nicolas Giard 2 år sedan
förälder
incheckning
468aebe2a8

+ 1 - 2
server/graph/resolvers/storage.js

@@ -92,8 +92,7 @@ module.exports = {
               }
             }
           }, {}),
-          actions: {}
-          // actions: md.actions
+          actions: md.actions
         }
       }), ['title'])
     }

+ 3 - 0
server/graph/resolvers/system.js

@@ -94,6 +94,9 @@ module.exports = {
     latestVersionReleaseDate () {
       return DateTime.fromISO(WIKI.system.updates.releaseDate).toJSDate()
     },
+    mailConfigured () {
+      return false // TODO: return true if mail is setup
+    },
     nodeVersion () {
       return process.version.substr(1)
     },

+ 1 - 0
server/graph/schemas/system.graphql

@@ -68,6 +68,7 @@ type SystemInfo {
   httpsPort: Int
   latestVersion: String
   latestVersionReleaseDate: Date
+  mailConfigured: Boolean
   nodeVersion: String
   operatingSystem: String
   pagesTotal: Int

+ 1 - 1
server/modules/storage/azure/definition.yml

@@ -50,7 +50,7 @@ props:
         - hot|Hot
         - cool|Cool
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to Azure
     hint: Output all content from the DB to Azure Blog Storage, overwriting any existing data. If you enabled Azure Blog Storage after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.
     icon: this-way-up

+ 1 - 1
server/modules/storage/db/definition.yml

@@ -18,7 +18,7 @@ versioning:
 sync: false
 props: {}
 actions:
-  - handler: purge
+  purge:
     label: Purge All Assets
     hint: Delete all asset data from the database (not the metadata). Useful if you moved assets to another storage target and want to reduce the size of the database.
     warn: This is a destructive action! Make sure all asset files are properly stored on another storage module! This action cannot be undone!

+ 3 - 3
server/modules/storage/disk/definition.yml

@@ -32,15 +32,15 @@ props:
     icon: archive-folder
     order: 2
 actions:
-  - handler: dump
+  dump:
     label: Dump all content to disk
     hint: Output all content from the DB to the local disk. If you enabled this module after content was created or you temporarily disabled this module, you'll want to execute this action to add the missing files.
     icon: downloads
-  - handler: backup
+  backup:
     label: Create Backup
     hint: Will create a manual backup archive at this point in time, in a subfolder named _manual, from the contents currently on disk.
     icon: archive-folder
-  - handler: importAll
+  importAll:
     label: Import Everything
     hint: Will import all content currently in the local disk folder.
     icon: database-daily-import

+ 1 - 1
server/modules/storage/gcs/definition.yml

@@ -59,7 +59,7 @@ props:
     default: storage.google.com
     order: 5
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to GCS
     hint: Output all content from the DB to Google Cloud Storage, overwriting any existing data. If you enabled Google Cloud Storage after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.
     icon: this-way-up

+ 4 - 4
server/modules/storage/git/definition.yml

@@ -134,19 +134,19 @@ props:
     icon: run-command
     order: 50
 actions:
-  - handler: syncUntracked
+  syncUntracked:
     label: Add Untracked Changes
     hint: Output all content from the DB to the local Git repository to ensure all untracked content is saved. If you enabled Git after content was created or you temporarily disabled Git, you'll want to execute this action to add the missing untracked changes.
     icon: database-daily-export
-  - handler: sync
+  sync:
     label: Force Sync
     hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected.
     icon: synchronize
-  - handler: importAll
+  importAll:
     label: Import Everything
     hint: Will import all content currently in the local Git repository, regardless of the latest commit state. Useful for importing content from the remote repository created before git was enabled.
     icon: database-daily-import
-  - handler: purge
+  purge:
     label: Purge Local Repository
     hint: If you have unrelated merge histories, clearing the local repository can resolve this issue. This will not affect the remote repository or perform any commit.
     icon: trash

+ 1 - 1
server/modules/storage/github/definition.yml

@@ -43,7 +43,7 @@ props:
     hint: The repository default branch.
     icon: code-fork
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to GitHub
     hint: Output all content from the DB to GitHub, overwriting any existing data. If you enabled GitHub after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.
     icon: this-way-up

+ 1 - 1
server/modules/storage/s3/definition.yml

@@ -153,7 +153,7 @@ props:
     if:
       - { key: 'mode', eq: 'custom' }
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to S3
     hint: Output all content from the DB to S3, overwriting any existing data. If you enabled S3 after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.
     icon: this-way-up

+ 1 - 1
server/modules/storage/sftp/definition.yml

@@ -87,7 +87,7 @@ props:
     hint: Base directory where files will be transferred to. The path must already exists and be writable by the user.
     icon: symlink-directory
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to Remote
     hint: Output all content from the DB to the remote SSH server, overwriting any existing data. If you enabled SFTP after content was created or you temporarily disabled it, you'll want to execute this action to add the missing content.
     icon: this-way-up

+ 2 - 0
ux/src/boot/components.js

@@ -1,9 +1,11 @@
 import { boot } from 'quasar/wrappers'
 
 import BlueprintIcon from '../components/BlueprintIcon.vue'
+import StatusLight from '../components/StatusLight.vue'
 import VNetworkGraph from 'v-network-graph'
 
 export default boot(({ app }) => {
   app.component('BlueprintIcon', BlueprintIcon)
+  app.component('StatusLight', StatusLight)
   app.use(VNetworkGraph)
 })

+ 65 - 0
ux/src/components/StatusLight.vue

@@ -0,0 +1,65 @@
+<template lang='pug'>
+.status-light(:class='cssClasses')
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+// PROPS
+
+const props = defineProps({
+  pulse: {
+    type: Boolean,
+    default: false
+  },
+  color: {
+    type: String,
+    default: ''
+  }
+})
+
+// COMPUTED
+
+const cssClasses = computed(() => {
+  return `${props.color} ${props.pulse && 'pulsate'}`
+})
+</script>
+
+<style lang="scss">
+.status-light {
+  display: block;
+  width: 5px;
+  height: 100%;
+  min-height: 5px;
+  border-radius: 5px;
+  color: $grey-5;
+  background-color: currentColor;
+  background-image: linear-gradient(to bottom, transparent, rgba(255,255,255,.4));
+
+  &.negative {
+    color: $negative;
+  }
+  &.positive {
+    color: $positive;
+  }
+  &.warning {
+    color: $warning;
+  }
+
+  &.pulsate {
+    animation: status-light-pulsate 2s ease infinite;
+  }
+}
+
+@keyframes status-light-pulsate {
+  0% {
+    box-shadow: 0 0 5px 0 currentColor;
+  }
+  50% {
+    box-shadow: 0 0 5px 2px currentColor;
+  }
+  100% {
+    box-shadow: 0 0 5px 0 currentColor;
+  }
+}
+</style>

+ 2 - 1
ux/src/i18n/locales/en.json

@@ -1466,5 +1466,6 @@
   "admin.api.key": "API Key",
   "admin.api.createSuccess": "API Key created successfully.",
   "admin.api.revoked": "Revoked",
-  "admin.api.revokedHint": "This key has been revoked and can no longer be used."
+  "admin.api.revokedHint": "This key has been revoked and can no longer be used.",
+  "admin.storage.setupRequired": "Setup required"
 }

+ 6 - 0
ux/src/layouts/AdminLayout.vue

@@ -132,6 +132,8 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-rest-api.svg')
           q-item-section {{ t('admin.api.title') }}
+          q-item-section(side)
+            status-light(:color='adminStore.info.isApiEnabled ? `positive` : `negative`')
         q-item(to='/_admin/audit', v-ripple, active-class='bg-primary text-white', disabled)
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-event-log.svg')
@@ -144,6 +146,8 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-message-settings.svg')
           q-item-section {{ t('admin.mail.title') }}
+          q-item-section(side)
+            status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`')
         q-item(to='/_admin/security', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-protect.svg')
@@ -156,6 +160,8 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-processor.svg')
           q-item-section {{ t('admin.system.title') }}
+          q-item-section(side)
+            status-light(:color='adminStore.isVersionLatest ? `positive` : `warning`')
         q-item(to='/_admin/utilities', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-swiss-army-knife.svg')

+ 3 - 5
ux/src/pages/AdminAuth.vue

@@ -41,14 +41,12 @@ q-page.admin-mail
             clickable
             )
             q-item-section(side)
-              q-icon(
-                :name='`img:` + str.strategy.icon'
-              )
+              q-icon(:name='`img:` + str.strategy.icon')
             q-item-section
               q-item-label {{str.displayName}}
               q-item-label(caption) {{str.strategy.title}}
             q-item-section(side)
-              q-spinner-rings(:color='str.isEnabled ? `positive` : `negative`', size='sm')
+              status-light(:color='str.isEnabled ? `positive` : `negative`', :pulse='str.isEnabled')
       q-btn.q-mt-sm.full-width(
         color='primary'
         icon='las la-plus'
@@ -499,7 +497,7 @@ function addStrategy (str) {
     config: transform(str.props, (cfg, v, k) => {
       cfg[k] = v.default
     }, {}),
-    isEnabled: true,
+    isEnabled: false,
     displayName: str.title,
     selfRegistration: false,
     domainWhitelist: [],

+ 3 - 1
ux/src/pages/AdminDashboard.vue

@@ -89,11 +89,13 @@ q-page.admin-dashboard
             )
     .col-12
       q-banner.bg-positive.text-white(
+        :class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`'
         inline-actions
         rounded
         )
         i.las.la-check.q-mr-sm
-        span.text-weight-medium Your Wiki.js server is running the latest version!
+        span.text-weight-medium(v-if='adminStore.isVersionLatest') Your Wiki.js server is running the latest version!
+        span.text-weight-medium(v-else) A new version of Wiki.js is available. Please update to the latest version.
         template(#action)
           q-btn(
             flat

+ 411 - 399
ux/src/pages/AdminStorage.vue

@@ -49,7 +49,7 @@ q-page.admin-storage
     .col-auto
       q-card.rounded-borders.bg-dark
         q-list(
-          style='min-width: 350px;'
+          style='min-width: 300px;'
           padding
           dark
           )
@@ -62,427 +62,430 @@ q-page.admin-storage
             clickable
             )
             q-item-section(side)
-              q-icon(
-                :name='`img:` + tgt.icon'
-              )
+              q-icon(:name='`img:` + tgt.icon')
             q-item-section
               q-item-label {{tgt.title}}
               q-item-label(caption, :class='getTargetSubtitleColor(tgt)') {{getTargetSubtitle(tgt)}}
             q-item-section(side)
-              q-spinner-rings(:color='tgt.isEnabled ? `positive` : `negative`', size='sm')
+              status-light(:color='tgt.isEnabled ? `positive` : `negative`', :pulse='tgt.isEnabled')
     .col(v-if='state.target')
-      //- -----------------------
-      //- Content Types
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.contentTypes')}}
-          .text-body2.text-grey {{ t('admin.storage.contentTypesHint') }}
-        q-item(tag='label')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.contentTypes.activeTypes'
-              :color='state.target.module === `db` ? `grey` : `primary`'
-              val='pages'
-              :aria-label='t(`admin.storage.contentTypePages`)'
-              :disable='state.target.module === `db`'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.contentTypePages`)}}
-            q-item-label(caption) {{t(`admin.storage.contentTypePagesHint`)}}
-        q-item(tag='label')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.contentTypes.activeTypes'
-              color='primary'
-              val='images'
-              :aria-label='t(`admin.storage.contentTypeImages`)'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.contentTypeImages`)}}
-            q-item-label(caption) {{t(`admin.storage.contentTypeImagesHint`)}}
-        q-item(tag='label')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.contentTypes.activeTypes'
-              color='primary'
-              val='documents'
-              :aria-label='t(`admin.storage.contentTypeDocuments`)'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.contentTypeDocuments`)}}
-            q-item-label(caption) {{t(`admin.storage.contentTypeDocumentsHint`)}}
-        q-item(tag='label')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.contentTypes.activeTypes'
-              color='primary'
-              val='others'
-              :aria-label='t(`admin.storage.contentTypeOthers`)'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.contentTypeOthers`)}}
-            q-item-label(caption) {{t(`admin.storage.contentTypeOthersHint`)}}
-        q-item(tag='label')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.contentTypes.activeTypes'
-              color='primary'
-              val='large'
-              :aria-label='t(`admin.storage.contentTypeLargeFiles`)'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.contentTypeLargeFiles`)}}
-            q-item-label(caption) {{t(`admin.storage.contentTypeLargeFilesHint`)}}
-            q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.contentTypeLargeFilesDBWarn`)}}
-          q-item-section(side)
-            q-input(
-              outlined
-              :label='t(`admin.storage.contentTypeLargeFilesThreshold`)'
-              v-model='state.target.contentTypes.largeThreshold'
-              style='min-width: 150px;'
-              dense
-            )
-
-      //- -----------------------
-      //- Content Delivery
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.assetDelivery')}}
-          .text-body2.text-grey {{ t('admin.storage.assetDeliveryHint') }}
-        q-item(:tag='state.target.assetDelivery.isStreamingSupported ? `label` : null')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.assetDelivery.streaming'
-              :color='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported ? `grey` : `primary`'
-              :aria-label='t(`admin.storage.contentTypePages`)'
-              :disable='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.assetStreaming`)}}
-            q-item-label(caption) {{t(`admin.storage.assetStreamingHint`)}}
-            q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isStreamingSupported', caption) {{t(`admin.storage.assetStreamingNotSupported`)}}
-        q-item(:tag='state.target.assetDelivery.isDirectAccessSupported ? `label` : null')
-          q-item-section(avatar)
-            q-checkbox(
-              v-model='state.target.assetDelivery.directAccess'
-              :color='!state.target.assetDelivery.isDirectAccessSupported ? `grey` : `primary`'
-              :aria-label='t(`admin.storage.contentTypePages`)'
-              :disable='!state.target.assetDelivery.isDirectAccessSupported'
-              )
-          q-item-section
-            q-item-label {{t(`admin.storage.assetDirectAccess`)}}
-            q-item-label(caption) {{t(`admin.storage.assetDirectAccessHint`)}}
-            q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isDirectAccessSupported', caption) {{t(`admin.storage.assetDirectAccessNotSupported`)}}
-
-      //- -----------------------
-      //- Setup
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`')
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.setup')}}
-          .text-body2.text-grey {{ t('admin.storage.setupHint') }}
-        template(v-if='state.target.setup.handler === `github` && state.target.setup.state === `notconfigured`')
-          q-item
-            blueprint-icon(icon='test-account')
-            q-item-section
-              q-item-label GitHub Account Type
-              q-item-label(caption) Whether to use an organization or personal GitHub account during setup.
-            q-item-section.col-auto
-              q-btn-toggle(
-                v-model='state.target.setup.values.accountType'
-                push
-                glossy
-                no-caps
-                toggle-color='primary'
-                :options=`[
-                  { label: t('admin.storage.githubAccTypeOrg'), value: 'org' },
-                  { label: t('admin.storage.githubAccTypePersonal'), value: 'personal' }
-                ]`
-              )
-          q-separator.q-my-sm(inset)
-          template(v-if='state.target.setup.values.accountType === `org`')
+      .row.q-col-gutter-md
+        .col-12.col-lg
+          //- -----------------------
+          //- Setup
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm.q-mb-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`')
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.setup')}}
+              .text-body2.text-grey {{ t('admin.storage.setupHint') }}
+            template(v-if='state.target.setup.handler === `github` && state.target.setup.state === `notconfigured`')
+              q-item
+                blueprint-icon(icon='test-account')
+                q-item-section
+                  q-item-label GitHub Account Type
+                  q-item-label(caption) Whether to use an organization or personal GitHub account during setup.
+                q-item-section.col-auto
+                  q-btn-toggle(
+                    v-model='state.target.setup.values.accountType'
+                    push
+                    glossy
+                    no-caps
+                    toggle-color='primary'
+                    :options=`[
+                      { label: t('admin.storage.githubAccTypeOrg'), value: 'org' },
+                      { label: t('admin.storage.githubAccTypePersonal'), value: 'personal' }
+                    ]`
+                  )
+              q-separator.q-my-sm(inset)
+              template(v-if='state.target.setup.values.accountType === `org`')
+                q-item
+                  blueprint-icon(icon='github')
+                  q-item-section
+                    q-item-label {{ t('admin.storage.githubOrg') }}
+                    q-item-label(caption) {{ t('admin.storage.githubOrgHint') }}
+                  q-item-section
+                    q-input(
+                      outlined
+                      v-model='state.target.setup.values.org'
+                      dense
+                      :aria-label='t(`admin.storage.githubOrg`)'
+                      )
+                q-separator.q-my-sm(inset)
+              q-item
+                blueprint-icon(icon='dns')
+                q-item-section
+                  q-item-label {{ t('admin.storage.githubPublicUrl') }}
+                  q-item-label(caption) {{ t('admin.storage.githubPublicUrlHint') }}
+                q-item-section
+                  q-input(
+                    outlined
+                    v-model='state.target.setup.values.publicUrl'
+                    dense
+                    :aria-label='t(`admin.storage.githubPublicUrl`)'
+                    )
+              q-card-section.q-pt-sm.text-right
+                form(
+                  ref='githubSetupForm'
+                  method='POST'
+                  :action='state.setupCfg.action'
+                  )
+                  input(
+                    type='hidden'
+                    name='manifest'
+                    :value='state.setupCfg.manifest'
+                  )
+                  q-btn(
+                    unelevated
+                    icon='las la-angle-double-right'
+                    :label='t(`admin.storage.startSetup`)'
+                    color='secondary'
+                    @click='setupGitHub'
+                    :loading='state.setupCfg.loading'
+                  )
+            template(v-else-if='state.target.setup.handler === `github` && state.target.setup.state === `pendinginstall`')
+              q-card-section.q-py-none
+                q-banner(
+                  rounded
+                  :class='$q.dark.isActive ? `bg-teal-9 text-white` : `bg-teal-1 text-teal-9`'
+                  ) {{t('admin.storage.githubFinish')}}
+              q-card-section.q-pt-sm.text-right
+                q-btn.q-mr-sm(
+                  unelevated
+                  icon='las la-times-circle'
+                  :label='t(`admin.storage.cancelSetup`)'
+                  color='negative'
+                  @click='setupDestroy'
+                )
+                q-btn(
+                  unelevated
+                  icon='las la-angle-double-right'
+                  :label='t(`admin.storage.finishSetup`)'
+                  color='secondary'
+                  @click='setupGitHubStep(`verify`)'
+                  :loading='state.setupCfg.loading'
+                )
+          q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state === `configured`')
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.setup')}}
+              .text-body2.text-grey {{ t('admin.storage.setupConfiguredHint') }}
             q-item
-              blueprint-icon(icon='github')
+              blueprint-icon.self-start(icon='matches', :hue-rotate='140')
               q-item-section
-                q-item-label {{ t('admin.storage.githubOrg') }}
-                q-item-label(caption) {{ t('admin.storage.githubOrgHint') }}
+                q-item-label Uninstall
+                q-item-label(caption) Delete the active configuration and start over the setup process.
+                q-item-label.text-red(caption): strong This action cannot be undone!
+              q-item-section(side)
+                q-btn.acrylic-btn(
+                  flat
+                  icon='las la-arrow-circle-right'
+                  color='negative'
+                  @click='setupDestroy'
+                  :label='t(`admin.storage.uninstall`)'
+                )
+
+          //- -----------------------
+          //- Content Types
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.contentTypes')}}
+              .text-body2.text-grey {{ t('admin.storage.contentTypesHint') }}
+            q-item(tag='label')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.contentTypes.activeTypes'
+                  :color='state.target.module === `db` ? `grey` : `primary`'
+                  val='pages'
+                  :aria-label='t(`admin.storage.contentTypePages`)'
+                  :disable='state.target.module === `db`'
+                  )
               q-item-section
+                q-item-label {{t(`admin.storage.contentTypePages`)}}
+                q-item-label(caption) {{t(`admin.storage.contentTypePagesHint`)}}
+            q-item(tag='label')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.contentTypes.activeTypes'
+                  color='primary'
+                  val='images'
+                  :aria-label='t(`admin.storage.contentTypeImages`)'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.contentTypeImages`)}}
+                q-item-label(caption) {{t(`admin.storage.contentTypeImagesHint`)}}
+            q-item(tag='label')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.contentTypes.activeTypes'
+                  color='primary'
+                  val='documents'
+                  :aria-label='t(`admin.storage.contentTypeDocuments`)'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.contentTypeDocuments`)}}
+                q-item-label(caption) {{t(`admin.storage.contentTypeDocumentsHint`)}}
+            q-item(tag='label')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.contentTypes.activeTypes'
+                  color='primary'
+                  val='others'
+                  :aria-label='t(`admin.storage.contentTypeOthers`)'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.contentTypeOthers`)}}
+                q-item-label(caption) {{t(`admin.storage.contentTypeOthersHint`)}}
+            q-item(tag='label')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.contentTypes.activeTypes'
+                  color='primary'
+                  val='large'
+                  :aria-label='t(`admin.storage.contentTypeLargeFiles`)'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.contentTypeLargeFiles`)}}
+                q-item-label(caption) {{t(`admin.storage.contentTypeLargeFilesHint`)}}
+                q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.contentTypeLargeFilesDBWarn`)}}
+              q-item-section(side)
                 q-input(
                   outlined
-                  v-model='state.target.setup.values.org'
+                  :label='t(`admin.storage.contentTypeLargeFilesThreshold`)'
+                  v-model='state.target.contentTypes.largeThreshold'
+                  style='min-width: 150px;'
                   dense
-                  :aria-label='t(`admin.storage.githubOrg`)'
-                  )
-            q-separator.q-my-sm(inset)
-          q-item
-            blueprint-icon(icon='dns')
-            q-item-section
-              q-item-label {{ t('admin.storage.githubPublicUrl') }}
-              q-item-label(caption) {{ t('admin.storage.githubPublicUrlHint') }}
-            q-item-section
-              q-input(
-                outlined
-                v-model='state.target.setup.values.publicUrl'
-                dense
-                :aria-label='t(`admin.storage.githubPublicUrl`)'
                 )
-          q-card-section.q-pt-sm.text-right
-            form(
-              ref='githubSetupForm'
-              method='POST'
-              :action='setupCfg.action'
-              )
-              input(
-                type='hidden'
-                name='manifest'
-                :value='setupCfg.manifest'
+
+          //- -----------------------
+          //- Content Delivery
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm.q-mt-md
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.assetDelivery')}}
+              .text-body2.text-grey {{ t('admin.storage.assetDeliveryHint') }}
+            q-item(:tag='state.target.assetDelivery.isStreamingSupported ? `label` : null')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.assetDelivery.streaming'
+                  :color='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported ? `grey` : `primary`'
+                  :aria-label='t(`admin.storage.contentTypePages`)'
+                  :disable='state.target.module === `db` || !state.target.assetDelivery.isStreamingSupported'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.assetStreaming`)}}
+                q-item-label(caption) {{t(`admin.storage.assetStreamingHint`)}}
+                q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isStreamingSupported', caption) {{t(`admin.storage.assetStreamingNotSupported`)}}
+            q-item(:tag='state.target.assetDelivery.isDirectAccessSupported ? `label` : null')
+              q-item-section(avatar)
+                q-checkbox(
+                  v-model='state.target.assetDelivery.directAccess'
+                  :color='!state.target.assetDelivery.isDirectAccessSupported ? `grey` : `primary`'
+                  :aria-label='t(`admin.storage.contentTypePages`)'
+                  :disable='!state.target.assetDelivery.isDirectAccessSupported'
+                  )
+              q-item-section
+                q-item-label {{t(`admin.storage.assetDirectAccess`)}}
+                q-item-label(caption) {{t(`admin.storage.assetDirectAccessHint`)}}
+                q-item-label.text-deep-orange(v-if='!state.target.assetDelivery.isDirectAccessSupported', caption) {{t(`admin.storage.assetDirectAccessNotSupported`)}}
+
+          //- -----------------------
+          //- Configuration
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm.q-mt-md
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.config')}}
+              q-banner.q-mt-md(
+                v-if='!state.target.config || Object.keys(state.target.config).length < 1'
+                rounded
+                :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
+                ) {{t('admin.storage.noConfigOption')}}
+            template(
+              v-for='(cfg, cfgKey, idx) in state.target.config'
               )
-              q-btn(
-                unelevated
-                icon='las la-angle-double-right'
-                :label='t(`admin.storage.startSetup`)'
-                color='secondary'
-                @click='setupGitHub'
-                :loading='setupCfg.loading'
+              template(
+                v-if='configIfCheck(cfg.if)'
+                )
+                q-separator.q-my-sm(inset, v-if='idx > 0')
+                q-item(v-if='cfg.type === `Boolean`', tag='label')
+                  blueprint-icon(:icon='cfg.icon', :hue-rotate='cfg.readOnly ? -45 : 0')
+                  q-item-section
+                    q-item-label {{cfg.title}}
+                    q-item-label(caption) {{cfg.hint}}
+                  q-item-section(avatar)
+                    q-toggle(
+                      v-model='cfg.value'
+                      color='primary'
+                      checked-icon='las la-check'
+                      unchecked-icon='las la-times'
+                      :aria-label='t(`admin.general.allowComments`)'
+                      :disable='cfg.readOnly'
+                      )
+                q-item(v-else)
+                  blueprint-icon(:icon='cfg.icon', :hue-rotate='cfg.readOnly ? -45 : 0')
+                  q-item-section
+                    q-item-label {{cfg.title}}
+                    q-item-label(caption) {{cfg.hint}}
+                  q-item-section(
+                    :style='cfg.type === `Number` ? `flex: 0 0 150px;` : ``'
+                    :class='{ "col-auto": cfg.enum && cfg.enumDisplay === `buttons` }'
+                    )
+                    q-btn-toggle(
+                      v-if='cfg.enum && cfg.enumDisplay === `buttons`'
+                      v-model='cfg.value'
+                      push
+                      glossy
+                      no-caps
+                      toggle-color='primary'
+                      :options=`cfg.enum`
+                      :disable='cfg.readOnly'
+                    )
+                    q-select(
+                      v-else-if='cfg.enum'
+                      outlined
+                      v-model='cfg.value'
+                      :options='cfg.enum'
+                      emit-value
+                      map-options
+                      dense
+                      options-dense
+                      :aria-label='cfg.title'
+                      :disable='cfg.readOnly'
+                    )
+                    q-input(
+                      v-else
+                      outlined
+                      v-model='cfg.value'
+                      dense
+                      :type='cfg.multiline ? `textarea` : `input`'
+                      :aria-label='cfg.title'
+                      :disable='cfg.readOnly'
+                      )
+
+          //- -----------------------
+          //- Sync
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.sync && Object.keys(state.target.sync).length > 0')
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.sync')}}
+              q-banner.q-mt-md(
+                rounded
+                :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
+                ) {{t('admin.storage.noSyncModes')}}
+
+          //- -----------------------
+          //- Actions
+          //- -----------------------
+          q-card.shadow-1.q-pb-sm.q-mt-md
+            q-card-section
+              .text-subtitle1 {{t('admin.storage.actions')}}
+              q-banner.q-mt-md(
+                v-if='!state.target.actions || state.target.actions.length < 1'
+                rounded
+                :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
+                ) {{t('admin.storage.noActions')}}
+              q-banner.q-mt-md(
+                v-else-if='!state.target.isEnabled'
+                rounded
+                :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
+                ) {{t('admin.storage.actionsInactiveWarn')}}
+
+            template(
+              v-if='state.target.isEnabled'
+              v-for='(act, idx) in state.target.actions'
               )
-        template(v-else-if='state.target.setup.handler === `github` && state.target.setup.state === `pendinginstall`')
-          q-card-section.q-py-none
-            q-banner(
-              rounded
-              :class='$q.dark.isActive ? `bg-teal-9 text-white` : `bg-teal-1 text-teal-9`'
-              ) {{t('admin.storage.githubFinish')}}
-          q-card-section.q-pt-sm.text-right
-            q-btn.q-mr-sm(
-              unelevated
-              icon='las la-times-circle'
-              :label='t(`admin.storage.cancelSetup`)'
-              color='negative'
-              @click='setupDestroy'
-            )
-            q-btn(
-              unelevated
-              icon='las la-angle-double-right'
-              :label='t(`admin.storage.finishSetup`)'
-              color='secondary'
-              @click='setupGitHubStep(`verify`)'
-              :loading='setupCfg.loading'
-            )
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.setup && state.target.setup.handler && state.target.setup.state === `configured`')
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.setup')}}
-          .text-body2.text-grey {{ t('admin.storage.setupConfiguredHint') }}
-        q-item
-          blueprint-icon.self-start(icon='matches', :hue-rotate='140')
-          q-item-section
-            q-item-label Uninstall
-            q-item-label(caption) Delete the active configuration and start over the setup process.
-            q-item-label.text-red(caption): strong This action cannot be undone!
-          q-item-section(side)
-            q-btn.acrylic-btn(
-              flat
-              icon='las la-arrow-circle-right'
-              color='negative'
-              @click='setupDestroy'
-              :label='t(`admin.storage.uninstall`)'
-            )
+              q-separator.q-my-sm(inset, v-if='idx > 0')
+              q-item
+                blueprint-icon.self-start(:icon='act.icon', :hue-rotate='45')
+                q-item-section
+                  q-item-label {{act.label}}
+                  q-item-label(caption) {{act.hint}}
+                  q-item-label.text-red(v-if='act.warn', caption): strong {{act.warn}}
+                q-item-section(side)
+                  q-btn.acrylic-btn(
+                    flat
+                    icon='las la-arrow-circle-right'
+                    color='primary'
+                    @click=''
+                    :label='t(`common.actions.proceed`)'
+                  )
 
-      //- -----------------------
-      //- Configuration
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.config')}}
-          q-banner.q-mt-md(
-            v-if='!state.target.config || Object.keys(state.target.config).length < 1'
-            rounded
-            :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{t('admin.storage.noConfigOption')}}
-        template(
-          v-for='(cfg, cfgKey, idx) in state.target.config'
-          )
-          template(
-            v-if='configIfCheck(cfg.if)'
-            )
-            q-separator.q-my-sm(inset, v-if='idx > 0')
-            q-item(v-if='cfg.type === `Boolean`', tag='label')
-              blueprint-icon(:icon='cfg.icon', :hue-rotate='cfg.readOnly ? -45 : 0')
+        .col-12.col-lg-auto
+          //- -----------------------
+          //- Infobox
+          //- -----------------------
+          q-card.rounded-borders.q-pb-md(style='width: 300px;')
+            q-card-section
+              .text-subtitle1 {{state.target.title}}
+              q-img.q-mt-sm.rounded-borders(
+                :src='state.target.banner'
+                fit='cover'
+                no-spinner
+              )
+              .text-body2.q-mt-md {{state.target.description}}
+            q-separator.q-mb-sm(inset)
+            q-item
+              q-item-section
+                q-item-label.text-grey {{t(`admin.storage.vendor`)}}
+                q-item-label {{state.target.vendor}}
+            q-separator.q-my-sm(inset)
+            q-item
               q-item-section
-                q-item-label {{cfg.title}}
-                q-item-label(caption) {{cfg.hint}}
+                q-item-label.text-grey {{t(`admin.storage.vendorWebsite`)}}
+                q-item-label: a(:href='state.target.website', target='_blank', rel='noreferrer') {{state.target.website}}
+
+          //- -----------------------
+          //- Status
+          //- -----------------------
+          q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 300px;')
+            q-card-section
+              .text-subtitle1 {{ t('admin.storage.status') }}
+            template(v-if='state.target.module !== `db`')
+              q-item(tag='label')
+                q-item-section
+                  q-item-label {{t(`admin.storage.enabled`)}}
+                  q-item-label(caption) {{t(`admin.storage.enabledHint`)}}
+                  q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.enabledForced`)}}
+                q-item-section(avatar)
+                  q-toggle(
+                    v-model='state.target.isEnabled'
+                    :disable='state.target.module === `db` || isSetupNeeded'
+                    color='primary'
+                    checked-icon='las la-check'
+                    unchecked-icon='las la-times'
+                    :aria-label='t(`admin.storage.enabled`)'
+                    )
+                q-inner-loading(:showing='isSetupNeeded')
+                  q-icon(name='las la-exclamation-triangle', size='sm', color='negative')
+                  .text-body2.text-negative {{ t('admin.storage.setupRequired') }}
+              q-separator.q-my-sm(inset)
+            q-item
+              q-item-section
+                q-item-label.text-grey {{t(`admin.storage.currentState`)}}
+                q-item-label.text-positive No issues detected.
+
+          //- -----------------------
+          //- Versioning
+          //- -----------------------
+          q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 300px;')
+            q-card-section
+              .text-subtitle1 {{t(`admin.storage.versioning`)}}
+              .text-body2.text-grey {{t(`admin.storage.versioningHint`)}}
+            q-item(:tag='state.target.versioning.isSupported ? `label` : null')
+              q-item-section
+                q-item-label {{t(`admin.storage.useVersioning`)}}
+                q-item-label(caption) {{t(`admin.storage.useVersioningHint`)}}
+                q-item-label.text-deep-orange(v-if='!state.target.versioning.isSupported', caption) {{t(`admin.storage.versioningNotSupported`)}}
+                q-item-label.text-deep-orange(v-if='state.target.versioning.isForceEnabled', caption) {{t(`admin.storage.versioningForceEnabled`)}}
               q-item-section(avatar)
                 q-toggle(
-                  v-model='cfg.value'
+                  v-model='state.target.versioning.enabled'
+                  :disable='!state.target.versioning.isSupported || state.target.versioning.isForceEnabled'
                   color='primary'
                   checked-icon='las la-check'
                   unchecked-icon='las la-times'
-                  :aria-label='t(`admin.general.allowComments`)'
-                  :disable='cfg.readOnly'
-                  )
-            q-item(v-else)
-              blueprint-icon(:icon='cfg.icon', :hue-rotate='cfg.readOnly ? -45 : 0')
-              q-item-section
-                q-item-label {{cfg.title}}
-                q-item-label(caption) {{cfg.hint}}
-              q-item-section(
-                :style='cfg.type === `Number` ? `flex: 0 0 150px;` : ``'
-                :class='{ "col-auto": cfg.enum && cfg.enumDisplay === `buttons` }'
-                )
-                q-btn-toggle(
-                  v-if='cfg.enum && cfg.enumDisplay === `buttons`'
-                  v-model='cfg.value'
-                  push
-                  glossy
-                  no-caps
-                  toggle-color='primary'
-                  :options=`cfg.enum`
-                  :disable='cfg.readOnly'
-                )
-                q-select(
-                  v-else-if='cfg.enum'
-                  outlined
-                  v-model='cfg.value'
-                  :options='cfg.enum'
-                  emit-value
-                  map-options
-                  dense
-                  options-dense
-                  :aria-label='cfg.title'
-                  :disable='cfg.readOnly'
-                )
-                q-input(
-                  v-else
-                  outlined
-                  v-model='cfg.value'
-                  dense
-                  :type='cfg.multiline ? `textarea` : `input`'
-                  :aria-label='cfg.title'
-                  :disable='cfg.readOnly'
+                  :aria-label='t(`admin.storage.useVersioning`)'
                   )
 
-      //- -----------------------
-      //- Sync
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.target.sync && Object.keys(state.target.sync).length > 0')
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.sync')}}
-          q-banner.q-mt-md(
-            rounded
-            :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{t('admin.storage.noSyncModes')}}
-
-      //- -----------------------
-      //- Actions
-      //- -----------------------
-      q-card.shadow-1.q-pb-sm.q-mt-md
-        q-card-section
-          .text-subtitle1 {{t('admin.storage.actions')}}
-          q-banner.q-mt-md(
-            v-if='!state.target.actions || state.target.actions.length < 1'
-            rounded
-            :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{t('admin.storage.noActions')}}
-          q-banner.q-mt-md(
-            v-else-if='!state.target.isEnabled'
-            rounded
-            :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
-            ) {{t('admin.storage.actionsInactiveWarn')}}
-
-        template(
-          v-if='state.target.isEnabled'
-          v-for='(act, idx) in state.target.actions'
-          )
-          q-separator.q-my-sm(inset, v-if='idx > 0')
-          q-item
-            blueprint-icon.self-start(:icon='act.icon', :hue-rotate='45')
-            q-item-section
-              q-item-label {{act.label}}
-              q-item-label(caption) {{act.hint}}
-              q-item-label.text-red(v-if='act.warn', caption): strong {{act.warn}}
-            q-item-section(side)
-              q-btn.acrylic-btn(
-                flat
-                icon='las la-arrow-circle-right'
-                color='primary'
-                @click=''
-                :label='t(`common.actions.proceed`)'
-              )
-
-    .col-auto(v-if='state.target')
-      //- -----------------------
-      //- Infobox
-      //- -----------------------
-      q-card.rounded-borders.q-pb-md(style='width: 350px;')
-        q-card-section
-          .text-subtitle1 {{state.target.title}}
-          q-img.q-mt-sm.rounded-borders(
-            :src='state.target.banner'
-            fit='cover'
-            no-spinner
-          )
-          .text-body2.q-mt-md {{state.target.description}}
-        q-separator.q-mb-sm(inset)
-        q-item
-          q-item-section
-            q-item-label.text-grey {{t(`admin.storage.vendor`)}}
-            q-item-label {{state.target.vendor}}
-        q-separator.q-my-sm(inset)
-        q-item
-          q-item-section
-            q-item-label.text-grey {{t(`admin.storage.vendorWebsite`)}}
-            q-item-label: a(:href='state.target.website', target='_blank', rel='noreferrer') {{state.target.website}}
-
-      //- -----------------------
-      //- Status
-      //- -----------------------
-      q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 350px;')
-        q-card-section
-          .text-subtitle1 {{ t('admin.storage.status') }}
-        template(v-if='state.target.module !== `db` && !(state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`)')
-          q-item(tag='label')
-            q-item-section
-              q-item-label {{t(`admin.storage.enabled`)}}
-              q-item-label(caption) {{t(`admin.storage.enabledHint`)}}
-              q-item-label.text-deep-orange(v-if='state.target.module === `db`', caption) {{t(`admin.storage.enabledForced`)}}
-            q-item-section(avatar)
-              q-toggle(
-                v-model='state.target.isEnabled'
-                :disable='state.target.module === `db` || (state.target.setup && state.target.setup.handler && state.target.setup.state !== `configured`)'
-                color='primary'
-                checked-icon='las la-check'
-                unchecked-icon='las la-times'
-                :aria-label='t(`admin.storage.enabled`)'
-                )
-          q-separator.q-my-sm(inset)
-        q-item
-          q-item-section
-            q-item-label.text-grey {{t(`admin.storage.currentState`)}}
-            q-item-label.text-positive No issues detected.
-
-      //- -----------------------
-      //- Versioning
-      //- -----------------------
-      q-card.rounded-borders.q-pb-md.q-mt-md(style='width: 350px;')
-        q-card-section
-          .text-subtitle1 {{t(`admin.storage.versioning`)}}
-          .text-body2.text-grey {{t(`admin.storage.versioningHint`)}}
-        q-item(:tag='state.target.versioning.isSupported ? `label` : null')
-          q-item-section
-            q-item-label {{t(`admin.storage.useVersioning`)}}
-            q-item-label(caption) {{t(`admin.storage.useVersioningHint`)}}
-            q-item-label.text-deep-orange(v-if='!state.target.versioning.isSupported', caption) {{t(`admin.storage.versioningNotSupported`)}}
-            q-item-label.text-deep-orange(v-if='state.target.versioning.isForceEnabled', caption) {{t(`admin.storage.versioningForceEnabled`)}}
-          q-item-section(avatar)
-            q-toggle(
-              v-model='state.target.versioning.enabled'
-              :disable='!state.target.versioning.isSupported || state.target.versioning.isForceEnabled'
-              color='primary'
-              checked-icon='las la-check'
-              unchecked-icon='las la-times'
-              :aria-label='t(`admin.storage.useVersioning`)'
-              )
-
   //- ==========================================
   //- DELIVERY PATHS
   //- ==========================================
@@ -705,6 +708,15 @@ const state = reactive({
 
 const githubSetupForm = ref(null)
 
+// COMPUTED
+
+const isSetupNeeded = computed(() => {
+  return state.target?.setup?.handler && state.target.setup.state !== 'configured'
+})
+const isSetupCompleted = computed(() => {
+  return state.target?.setup?.handler && state.target.setup.state !== 'configured'
+})
+
 // WATCHERS
 
 watch(() => adminStore.currentSiteId, async (newValue) => {

+ 23 - 5
ux/src/stores/admin.js

@@ -1,6 +1,7 @@
 import { defineStore } from 'pinia'
 import gql from 'graphql-tag'
-import { cloneDeep } from 'lodash-es'
+import { clone, cloneDeep } from 'lodash-es'
+import semverGte from 'semver/functions/gte'
 
 /* global APOLLO_CLIENT */
 
@@ -13,7 +14,9 @@ export const useAdminStore = defineStore('admin', {
       groupsTotal: 0,
       pagesTotal: 0,
       usersTotal: 0,
-      loginsPastDay: 0
+      loginsPastDay: 0,
+      isApiEnabled: false,
+      isMailConfigured: false
     },
     overlay: null,
     overlayOpts: {},
@@ -22,7 +25,14 @@ export const useAdminStore = defineStore('admin', {
       { code: 'en', name: 'English' }
     ]
   }),
-  getters: {},
+  getters: {
+    isVersionLatest: (state) => {
+      if (!state.info.currentVersion || !state.info.latestVersion || state.info.currentVersion === 'n/a' || state.info.latestVersion === 'n/a') {
+        return false
+      }
+      return semverGte(state.info.currentVersion, state.info.latestVersion)
+    }
+  },
   actions: {
     async fetchSites () {
       const resp = await APOLLO_CLIENT.query({
@@ -47,16 +57,24 @@ export const useAdminStore = defineStore('admin', {
       const resp = await APOLLO_CLIENT.query({
         query: gql`
           query getAdminInfo {
+            apiState
             systemInfo {
               groupsTotal
               usersTotal
+              currentVersion
+              latestVersion
+              mailConfigured
             }
           }
         `,
         fetchPolicy: 'network-only'
       })
-      this.info.groupsTotal = cloneDeep(resp?.data?.systemInfo.groupsTotal ?? 0)
-      this.info.usersTotal = cloneDeep(resp?.data?.systemInfo.usersTotal ?? 0)
+      this.info.groupsTotal = clone(resp?.data?.systemInfo?.groupsTotal ?? 0)
+      this.info.usersTotal = clone(resp?.data?.systemInfo?.usersTotal ?? 0)
+      this.info.currentVersion = clone(resp?.data?.systemInfo?.currentVersion ?? 'n/a')
+      this.info.latestVersion = clone(resp?.data?.systemInfo?.latestVersion ?? 'n/a')
+      this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)
+      this.info.isMailConfigured = clone(resp?.data?.systemInfo?.mailConfigured ?? false)
     }
   }
 })