Browse Source

feat: admin storage actions + status light component

Nicolas Giard 2 years ago
parent
commit
468aebe2a8

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

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

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

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

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

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

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

@@ -50,7 +50,7 @@ props:
         - hot|Hot
         - hot|Hot
         - cool|Cool
         - cool|Cool
 actions:
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to Azure
     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.
     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
     icon: this-way-up

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

@@ -18,7 +18,7 @@ versioning:
 sync: false
 sync: false
 props: {}
 props: {}
 actions:
 actions:
-  - handler: purge
+  purge:
     label: Purge All Assets
     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.
     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!
     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
     icon: archive-folder
     order: 2
     order: 2
 actions:
 actions:
-  - handler: dump
+  dump:
     label: Dump all content to disk
     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.
     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
     icon: downloads
-  - handler: backup
+  backup:
     label: Create 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.
     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
     icon: archive-folder
-  - handler: importAll
+  importAll:
     label: Import Everything
     label: Import Everything
     hint: Will import all content currently in the local disk folder.
     hint: Will import all content currently in the local disk folder.
     icon: database-daily-import
     icon: database-daily-import

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

@@ -59,7 +59,7 @@ props:
     default: storage.google.com
     default: storage.google.com
     order: 5
     order: 5
 actions:
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to GCS
     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.
     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
     icon: this-way-up

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

@@ -134,19 +134,19 @@ props:
     icon: run-command
     icon: run-command
     order: 50
     order: 50
 actions:
 actions:
-  - handler: syncUntracked
+  syncUntracked:
     label: Add Untracked Changes
     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.
     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
     icon: database-daily-export
-  - handler: sync
+  sync:
     label: Force Sync
     label: Force Sync
     hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected.
     hint: Will trigger an immediate sync operation, regardless of the current sync schedule. The sync direction is respected.
     icon: synchronize
     icon: synchronize
-  - handler: importAll
+  importAll:
     label: Import Everything
     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.
     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
     icon: database-daily-import
-  - handler: purge
+  purge:
     label: Purge Local Repository
     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.
     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
     icon: trash

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

@@ -43,7 +43,7 @@ props:
     hint: The repository default branch.
     hint: The repository default branch.
     icon: code-fork
     icon: code-fork
 actions:
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to GitHub
     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.
     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
     icon: this-way-up

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

@@ -153,7 +153,7 @@ props:
     if:
     if:
       - { key: 'mode', eq: 'custom' }
       - { key: 'mode', eq: 'custom' }
 actions:
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to S3
     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.
     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
     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.
     hint: Base directory where files will be transferred to. The path must already exists and be writable by the user.
     icon: symlink-directory
     icon: symlink-directory
 actions:
 actions:
-  - handler: exportAll
+  exportAll:
     label: Export All DB Assets to Remote
     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.
     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
     icon: this-way-up

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

@@ -1,9 +1,11 @@
 import { boot } from 'quasar/wrappers'
 import { boot } from 'quasar/wrappers'
 
 
 import BlueprintIcon from '../components/BlueprintIcon.vue'
 import BlueprintIcon from '../components/BlueprintIcon.vue'
+import StatusLight from '../components/StatusLight.vue'
 import VNetworkGraph from 'v-network-graph'
 import VNetworkGraph from 'v-network-graph'
 
 
 export default boot(({ app }) => {
 export default boot(({ app }) => {
   app.component('BlueprintIcon', BlueprintIcon)
   app.component('BlueprintIcon', BlueprintIcon)
+  app.component('StatusLight', StatusLight)
   app.use(VNetworkGraph)
   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.key": "API Key",
   "admin.api.createSuccess": "API Key created successfully.",
   "admin.api.createSuccess": "API Key created successfully.",
   "admin.api.revoked": "Revoked",
   "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-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-rest-api.svg')
             q-icon(name='img:/_assets/icons/fluent-rest-api.svg')
           q-item-section {{ t('admin.api.title') }}
           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(to='/_admin/audit', v-ripple, active-class='bg-primary text-white', disabled)
           q-item-section(avatar)
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-event-log.svg')
             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-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-message-settings.svg')
             q-icon(name='img:/_assets/icons/fluent-message-settings.svg')
           q-item-section {{ t('admin.mail.title') }}
           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(to='/_admin/security', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-protect.svg')
             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-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-processor.svg')
             q-icon(name='img:/_assets/icons/fluent-processor.svg')
           q-item-section {{ t('admin.system.title') }}
           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(to='/_admin/utilities', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-swiss-army-knife.svg')
             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
             clickable
             )
             )
             q-item-section(side)
             q-item-section(side)
-              q-icon(
-                :name='`img:` + str.strategy.icon'
-              )
+              q-icon(:name='`img:` + str.strategy.icon')
             q-item-section
             q-item-section
               q-item-label {{str.displayName}}
               q-item-label {{str.displayName}}
               q-item-label(caption) {{str.strategy.title}}
               q-item-label(caption) {{str.strategy.title}}
             q-item-section(side)
             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(
       q-btn.q-mt-sm.full-width(
         color='primary'
         color='primary'
         icon='las la-plus'
         icon='las la-plus'
@@ -499,7 +497,7 @@ function addStrategy (str) {
     config: transform(str.props, (cfg, v, k) => {
     config: transform(str.props, (cfg, v, k) => {
       cfg[k] = v.default
       cfg[k] = v.default
     }, {}),
     }, {}),
-    isEnabled: true,
+    isEnabled: false,
     displayName: str.title,
     displayName: str.title,
     selfRegistration: false,
     selfRegistration: false,
     domainWhitelist: [],
     domainWhitelist: [],

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

@@ -89,11 +89,13 @@ q-page.admin-dashboard
             )
             )
     .col-12
     .col-12
       q-banner.bg-positive.text-white(
       q-banner.bg-positive.text-white(
+        :class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`'
         inline-actions
         inline-actions
         rounded
         rounded
         )
         )
         i.las.la-check.q-mr-sm
         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)
         template(#action)
           q-btn(
           q-btn(
             flat
             flat

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

@@ -49,7 +49,7 @@ q-page.admin-storage
     .col-auto
     .col-auto
       q-card.rounded-borders.bg-dark
       q-card.rounded-borders.bg-dark
         q-list(
         q-list(
-          style='min-width: 350px;'
+          style='min-width: 300px;'
           padding
           padding
           dark
           dark
           )
           )
@@ -62,427 +62,430 @@ q-page.admin-storage
             clickable
             clickable
             )
             )
             q-item-section(side)
             q-item-section(side)
-              q-icon(
-                :name='`img:` + tgt.icon'
-              )
+              q-icon(:name='`img:` + tgt.icon')
             q-item-section
             q-item-section
               q-item-label {{tgt.title}}
               q-item-label {{tgt.title}}
               q-item-label(caption, :class='getTargetSubtitleColor(tgt)') {{getTargetSubtitle(tgt)}}
               q-item-label(caption, :class='getTargetSubtitleColor(tgt)') {{getTargetSubtitle(tgt)}}
             q-item-section(side)
             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')
     .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
             q-item
-              blueprint-icon(icon='github')
+              blueprint-icon.self-start(icon='matches', :hue-rotate='140')
               q-item-section
               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-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(
                 q-input(
                   outlined
                   outlined
-                  v-model='state.target.setup.values.org'
+                  :label='t(`admin.storage.contentTypeLargeFilesThreshold`)'
+                  v-model='state.target.contentTypes.largeThreshold'
+                  style='min-width: 150px;'
                   dense
                   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-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-item-section(avatar)
                 q-toggle(
                 q-toggle(
-                  v-model='cfg.value'
+                  v-model='state.target.versioning.enabled'
+                  :disable='!state.target.versioning.isSupported || state.target.versioning.isForceEnabled'
                   color='primary'
                   color='primary'
                   checked-icon='las la-check'
                   checked-icon='las la-check'
                   unchecked-icon='las la-times'
                   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
   //- DELIVERY PATHS
   //- ==========================================
   //- ==========================================
@@ -705,6 +708,15 @@ const state = reactive({
 
 
 const githubSetupForm = ref(null)
 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
 // WATCHERS
 
 
 watch(() => adminStore.currentSiteId, async (newValue) => {
 watch(() => adminStore.currentSiteId, async (newValue) => {

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

@@ -1,6 +1,7 @@
 import { defineStore } from 'pinia'
 import { defineStore } from 'pinia'
 import gql from 'graphql-tag'
 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 */
 /* global APOLLO_CLIENT */
 
 
@@ -13,7 +14,9 @@ export const useAdminStore = defineStore('admin', {
       groupsTotal: 0,
       groupsTotal: 0,
       pagesTotal: 0,
       pagesTotal: 0,
       usersTotal: 0,
       usersTotal: 0,
-      loginsPastDay: 0
+      loginsPastDay: 0,
+      isApiEnabled: false,
+      isMailConfigured: false
     },
     },
     overlay: null,
     overlay: null,
     overlayOpts: {},
     overlayOpts: {},
@@ -22,7 +25,14 @@ export const useAdminStore = defineStore('admin', {
       { code: 'en', name: 'English' }
       { 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: {
   actions: {
     async fetchSites () {
     async fetchSites () {
       const resp = await APOLLO_CLIENT.query({
       const resp = await APOLLO_CLIENT.query({
@@ -47,16 +57,24 @@ export const useAdminStore = defineStore('admin', {
       const resp = await APOLLO_CLIENT.query({
       const resp = await APOLLO_CLIENT.query({
         query: gql`
         query: gql`
           query getAdminInfo {
           query getAdminInfo {
+            apiState
             systemInfo {
             systemInfo {
               groupsTotal
               groupsTotal
               usersTotal
               usersTotal
+              currentVersion
+              latestVersion
+              mailConfigured
             }
             }
           }
           }
         `,
         `,
         fetchPolicy: 'network-only'
         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)
     }
     }
   }
   }
 })
 })