feat: storage schedule + status
This commit is contained in:
		| @@ -15,7 +15,7 @@ | ||||
|             span {{$t('common:actions.apply')}} | ||||
|  | ||||
|         v-card.mt-3 | ||||
|           v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark) | ||||
|           v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark, v-model='currentTab') | ||||
|             v-tab(key='settings'): v-icon settings | ||||
|             v-tab(v-for='tgt in activeTargets', :key='tgt.key') {{ tgt.title }} | ||||
|  | ||||
| @@ -37,16 +37,29 @@ | ||||
|                       ) | ||||
|                   v-flex(xs12, md6) | ||||
|                     .pa-3.grey.radius-7(:class='$vuetify.dark ? "darken-4" : "lighten-5"') | ||||
|                       .body-2.grey--text.text--darken-1 Advanced Settings | ||||
|                       v-text-field.mt-3.md2( | ||||
|                         v-model='syncInterval' | ||||
|                         outline | ||||
|                         background-color='grey lighten-2' | ||||
|                         prepend-icon='schedule' | ||||
|                         label='Synchronization Interval' | ||||
|                         hint='For performance reasons, some storage targets synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur for all storage targets.' | ||||
|                         persistent-hint | ||||
|                       ) | ||||
|                       v-layout.pa-2(row, justify-space-between) | ||||
|                         .body-2.grey--text.text--darken-1 Status | ||||
|                         looping-rhombuses-spinner.mt-1( | ||||
|                           :animation-duration='5000' | ||||
|                           :rhombus-size='10' | ||||
|                           color='#BBB' | ||||
|                         ) | ||||
|                       v-divider | ||||
|                       v-toolbar.mt-2.radius-7( | ||||
|                         v-for='(tgt, n) in status' | ||||
|                         :key='tgt.key' | ||||
|                         dense | ||||
|                         :color='getStatusColor(tgt.status)' | ||||
|                         dark | ||||
|                         flat | ||||
|                         :extended='tgt.status === `error`', | ||||
|                         extension-height='100' | ||||
|                         ) | ||||
|                         .pa-3.red.darken-2.radius-7(v-if='tgt.status === `error`', slot='extension') {{tgt.message}} | ||||
|                         .body-2 {{tgt.title}} | ||||
|                         v-spacer | ||||
|                         .body-1 {{tgt.status}} | ||||
|                       v-alert.mt-3.radius-7(v-if='status.length < 1', outline, :value='true', color='indigo') You don't have any active storage target. | ||||
|  | ||||
|             v-tab-item(v-for='(tgt, n) in activeTargets', :key='tgt.key', :transition='false', :reverse-transition='false') | ||||
|               v-card.pa-3(flat, tile) | ||||
| @@ -125,22 +138,40 @@ | ||||
|                     .pb-3 Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios. | ||||
|                     strong Pull from target #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`pull`) < 0') Unsupported] | ||||
|                     .pb-3 Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten! | ||||
|  | ||||
|                   template(v-if='tgt.hasSchedule') | ||||
|                     v-divider.mt-3 | ||||
|                     v-subheader.pl-0 Sync Schedule | ||||
|                     .body-1.ml-3 For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur. | ||||
|                     .pa-3 | ||||
|                       duration-picker(v-model='tgt.syncInterval') | ||||
|                       .caption.mt-3 The default is every #[strong 5 minutes]. | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from 'lodash' | ||||
|  | ||||
| import DurationPicker from '../common/duration-picker.vue' | ||||
| import { LoopingRhombusesSpinner } from 'epic-spinners' | ||||
|  | ||||
| import statusQuery from 'gql/admin/storage/storage-query-status.gql' | ||||
| import targetsQuery from 'gql/admin/storage/storage-query-targets.gql' | ||||
| import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     DurationPicker, | ||||
|     LoopingRhombusesSpinner | ||||
|   }, | ||||
|   filters: { | ||||
|     startCase(val) { return _.startCase(val) } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       syncInterval: '5m', | ||||
|       targets: [] | ||||
|       currentTab: 0, | ||||
|       targets: [], | ||||
|       status: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -163,19 +194,33 @@ export default { | ||||
|         mutation: targetsSaveMutation, | ||||
|         variables: { | ||||
|           targets: this.targets.map(tgt => _.pick(tgt, [ | ||||
|             'isEnabled', | ||||
|             'key', | ||||
|             'config', | ||||
|             'mode' | ||||
|           ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))})) | ||||
|               'isEnabled', | ||||
|               'key', | ||||
|               'config', | ||||
|               'mode', | ||||
|               'syncInterval' | ||||
|             ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))})) | ||||
|         } | ||||
|       }) | ||||
|       this.currentTab = 0 | ||||
|       this.$store.commit('showNotification', { | ||||
|         message: 'Storage configuration saved successfully.', | ||||
|         style: 'success', | ||||
|         icon: 'check' | ||||
|       }) | ||||
|       this.$store.commit(`loadingStop`, 'admin-storage-savetargets') | ||||
|     }, | ||||
|     getStatusColor(state) { | ||||
|       switch (state) { | ||||
|         case 'pending': | ||||
|           return 'purple lighten-2' | ||||
|         case 'operational': | ||||
|           return 'green' | ||||
|         case 'error': | ||||
|           return 'red' | ||||
|         default: | ||||
|           return 'grey darken-2' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   apollo: { | ||||
| @@ -190,8 +235,17 @@ export default { | ||||
|         })), [t => t.value.order]) | ||||
|       })), | ||||
|       watchLoading (isLoading) { | ||||
|         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-refresh') | ||||
|         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh') | ||||
|       } | ||||
|     }, | ||||
|     status: { | ||||
|       query: statusQuery, | ||||
|       fetchPolicy: 'network-only', | ||||
|       update: (data) => data.storage.status, | ||||
|       watchLoading (isLoading) { | ||||
|         this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh') | ||||
|       }, | ||||
|       pollInterval: 3000 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -82,19 +82,6 @@ | ||||
|  | ||||
|                 v-divider.mt-3 | ||||
|  | ||||
|                 v-subheader Redis | ||||
|                 v-list-tile(avatar) | ||||
|                   v-list-tile-avatar | ||||
|                     v-avatar.red(size='40') | ||||
|                       icon-cube(fillColor='#FFFFFF') | ||||
|                   v-list-tile-content | ||||
|                     v-list-tile-title {{ info.redisVersion }} | ||||
|                     v-list-tile-sub-title {{ info.redisHost }} | ||||
|                   v-list-tile-action | ||||
|                     v-list-tile-action-text {{ $t('admin:system.ramUsage', { used: info.redisUsedRAM, total: info.redisTotalRAM }) }} | ||||
|  | ||||
|                 v-divider.mt-3 | ||||
|  | ||||
|                 v-subheader {{ info.dbType }} | ||||
|                 v-list-tile(avatar) | ||||
|                   v-list-tile-avatar | ||||
| @@ -108,7 +95,6 @@ | ||||
| <script> | ||||
| import _ from 'lodash' | ||||
|  | ||||
| import IconCube from 'mdi/Cube' | ||||
| import IconDatabase from 'mdi/Database' | ||||
| import IconNodeJs from 'mdi/Nodejs' | ||||
|  | ||||
| @@ -116,7 +102,6 @@ import systemInfoQuery from 'gql/admin/system/system-query-info.gql' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     IconCube, | ||||
|     IconDatabase, | ||||
|     IconNodeJs | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										114
									
								
								client/components/common/duration-picker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								client/components/common/duration-picker.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| <template lang='pug'> | ||||
|   v-toolbar(flat, :color='$vuetify.dark ? "grey darken-4-l3" : "grey lighten-3"') | ||||
|     .body-2.mr-3 Every | ||||
|     v-text-field( | ||||
|       solo | ||||
|       hide-details | ||||
|       flat | ||||
|       reverse | ||||
|       v-model='minutes' | ||||
|     ) | ||||
|     .body-2.mx-3 Minute(s) | ||||
|     v-divider.mr-3() | ||||
|     v-text-field( | ||||
|       solo | ||||
|       hide-details | ||||
|       flat | ||||
|       reverse | ||||
|       v-model='hours' | ||||
|     ) | ||||
|     .body-2.mx-3 Hour(s) | ||||
|     v-divider.mr-3() | ||||
|     v-text-field( | ||||
|       solo | ||||
|       hide-details | ||||
|       flat | ||||
|       reverse | ||||
|       v-model='days' | ||||
|     ) | ||||
|     .body-2.mx-3 Day(s) | ||||
|     v-divider.mr-3() | ||||
|     v-text-field( | ||||
|       solo | ||||
|       hide-details | ||||
|       flat | ||||
|       reverse | ||||
|       v-model='months' | ||||
|     ) | ||||
|     .body-2.mx-3 Month(s) | ||||
|     v-divider.mr-3() | ||||
|     v-text-field( | ||||
|       solo | ||||
|       hide-details | ||||
|       flat | ||||
|       reverse | ||||
|       v-model='years' | ||||
|     ) | ||||
|     .body-2.mx-3 Year(s) | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from 'lodash' | ||||
| import moment from 'moment' | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     value: { | ||||
|       type: String, | ||||
|       default: 'PT5M' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       duration: moment.duration(0) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     years: { | ||||
|       get() { return this.duration.years() || 0 }, | ||||
|       set(val) { this.rebuild(_.toNumber(val), 'years') } | ||||
|     }, | ||||
|     months: { | ||||
|       get() { return this.duration.months() || 0 }, | ||||
|       set(val) { this.rebuild(_.toNumber(val), 'months') } | ||||
|     }, | ||||
|     days: { | ||||
|       get() { return this.duration.days() || 0 }, | ||||
|       set(val) { this.rebuild(_.toNumber(val), 'days') } | ||||
|     }, | ||||
|     hours: { | ||||
|       get() { return this.duration.hours() || 0 }, | ||||
|       set(val) { this.rebuild(_.toNumber(val), 'hours') } | ||||
|     }, | ||||
|     minutes: { | ||||
|       get() { return this.duration.minutes() || 0 }, | ||||
|       set(val) { this.rebuild(_.toNumber(val), 'minutes') } | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     value(newValue, oldValue) { | ||||
|       this.duration = moment.duration(newValue) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     rebuild(val, unit) { | ||||
|       if (!_.isFinite(val) || val < 0) { | ||||
|         val = 0 | ||||
|       } | ||||
|       const newDuration = { | ||||
|         minutes: this.duration.minutes(), | ||||
|         hours: this.duration.hours(), | ||||
|         days: this.duration.days(), | ||||
|         months: this.duration.months(), | ||||
|         years: this.duration.years() | ||||
|       } | ||||
|       _.set(newDuration, unit, val) | ||||
|       this.duration = moment.duration(newDuration) | ||||
|       this.$emit('input', this.duration.toISOString()) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.duration = moment.duration(this.value) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
		Reference in New Issue
	
	Block a user