You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
14 KiB

  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row, wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft Storage
  9. .subheading.grey--text.animated.fadeInLeft.wait-p4s Set backup and sync targets for your content
  10. v-spacer
  11. v-btn.animated.fadeInDown.wait-p2s(outline, color='grey', @click='refresh', large)
  12. v-icon refresh
  13. v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
  14. v-icon(left) check
  15. span {{$t('common:actions.apply')}}
  16. v-flex(lg3, xs12)
  17. v-card.animated.fadeInUp
  18. v-toolbar(flat, color='primary', dark, dense)
  19. .subheading Targets
  20. v-list(two-line, dense).py-0
  21. template(v-for='(tgt, idx) in targets')
  22. v-list-tile(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')
  23. v-list-tile-avatar
  24. v-icon(color='grey', v-if='!tgt.isAvailable') indeterminate_check_box
  25. v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') check_box
  26. v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') check_box_outline_blank
  27. v-list-tile-content
  28. v-list-tile-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}
  29. v-list-tile-sub-title.caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}
  30. v-list-tile-avatar(v-if='selectedTarget === tgt.key')
  31. v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
  32. v-divider(v-if='idx < targets.length - 1')
  33. v-card.mt-3.animated.fadeInUp.wait-p2s
  34. v-toolbar(flat, :color='$vuetify.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)
  35. .subheading Status
  36. v-spacer
  37. looping-rhombuses-spinner(
  38. :animation-duration='5000'
  39. :rhombus-size='10'
  40. color='#FFF'
  41. )
  42. v-list.py-0(two-line, dense)
  43. template(v-for='(tgt, n) in status')
  44. v-list-tile(:key='tgt.key')
  45. template(v-if='tgt.status === `pending`')
  46. v-list-tile-avatar(color='purple')
  47. v-icon(color='white') schedule
  48. v-list-tile-content
  49. v-list-tile-title.body-2 {{tgt.title}}
  50. v-list-tile-sub-title.purple--text.caption {{tgt.status}}
  51. v-list-tile-action
  52. v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
  53. template(v-else-if='tgt.status === `operational`')
  54. v-list-tile-avatar(color='green')
  55. v-icon(color='white') check_circle
  56. v-list-tile-content
  57. v-list-tile-title.body-2 {{tgt.title}}
  58. v-list-tile-sub-title.green--text.caption Last synchronization {{tgt.lastAttempt | moment('from') }}
  59. template(v-else)
  60. v-list-tile-avatar(color='red')
  61. v-icon(color='white') highlight_off
  62. v-list-tile-content
  63. v-list-tile-title.body-2 {{tgt.title}}
  64. v-list-tile-sub-title.red--text.caption Last attempt was {{tgt.lastAttempt | moment('from') }}
  65. v-list-tile-action
  66. v-menu
  67. v-btn(slot='activator', icon)
  68. v-icon(color='red') info
  69. v-card(width='450')
  70. v-toolbar(flat, color='red', dark, dense) Error Message
  71. v-card-text {{tgt.message}}
  72. v-divider(v-if='n < status.length - 1')
  73. v-list-tile(v-if='status.length < 1')
  74. em You don't have any active storage target.
  75. v-flex(xs12, lg9)
  76. v-card.wiki-form.animated.fadeInUp.wait-p2s
  77. v-toolbar(color='primary', dense, flat, dark)
  78. .subheading {{target.title}}
  79. v-card-text
  80. v-form
  81. .targetlogo
  82. img(:src='target.logo', :alt='target.title')
  83. v-subheader.pl-0 {{target.title}}
  84. .caption {{target.description}}
  85. .caption: a(:href='target.website') {{target.website}}
  86. v-divider.mt-3
  87. v-subheader.pl-0 Target Configuration
  88. .body-1.ml-3(v-if='!target.config || target.config.length < 1') This storage target has no configuration options you can modify.
  89. template(v-else, v-for='cfg in target.config')
  90. v-select(
  91. v-if='cfg.value.type === "string" && cfg.value.enum'
  92. outline
  93. background-color='grey lighten-2'
  94. :items='cfg.value.enum'
  95. :key='cfg.key'
  96. :label='cfg.value.title'
  97. v-model='cfg.value.value'
  98. prepend-icon='settings_applications'
  99. :hint='cfg.value.hint ? cfg.value.hint : ""'
  100. persistent-hint
  101. :class='cfg.value.hint ? "mb-2" : ""'
  102. )
  103. v-switch.mb-3(
  104. v-else-if='cfg.value.type === "boolean"'
  105. :key='cfg.key'
  106. :label='cfg.value.title'
  107. v-model='cfg.value.value'
  108. color='primary'
  109. prepend-icon='settings_applications'
  110. :hint='cfg.value.hint ? cfg.value.hint : ""'
  111. persistent-hint
  112. )
  113. v-text-field(
  114. v-else
  115. outline
  116. background-color='grey lighten-2'
  117. :key='cfg.key'
  118. :label='cfg.value.title'
  119. v-model='cfg.value.value'
  120. prepend-icon='settings_applications'
  121. :hint='cfg.value.hint ? cfg.value.hint : ""'
  122. persistent-hint
  123. :class='cfg.value.hint ? "mb-2" : ""'
  124. )
  125. v-divider.mt-3
  126. v-subheader.pl-0 Sync Direction
  127. .body-1.ml-3 Choose how content synchronization is handled for this storage target.
  128. .pr-3.pt-3
  129. v-radio-group.ml-3.py-0(v-model='target.mode')
  130. v-radio(
  131. label='Bi-directional'
  132. color='primary'
  133. value='sync'
  134. :disabled='target.supportedModes.indexOf(`sync`) < 0'
  135. )
  136. v-radio(
  137. label='Push to target'
  138. color='primary'
  139. value='push'
  140. :disabled='target.supportedModes.indexOf(`push`) < 0'
  141. )
  142. v-radio(
  143. label='Pull from target'
  144. color='primary'
  145. value='pull'
  146. :disabled='target.supportedModes.indexOf(`pull`) < 0'
  147. )
  148. .body-1.ml-3
  149. strong Bi-directional #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') Unsupported]
  150. .pb-3 In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.
  151. strong Push to target #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') Unsupported]
  152. .pb-3 Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.
  153. strong Pull from target #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') Unsupported]
  154. .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!
  155. template(v-if='target.hasSchedule')
  156. v-divider.mt-3
  157. v-subheader.pl-0 Sync Schedule
  158. .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.
  159. .pa-3
  160. duration-picker(v-model='target.syncInterval')
  161. .caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(target.syncInterval)}}].
  162. .caption The default is every #[strong {{getDefaultSchedule(target.syncIntervalDefault)}}].
  163. template(v-if='target.actions && target.actions.length > 0')
  164. v-divider.mt-3
  165. v-subheader.pl-0 Actions
  166. v-container.pt-0(grid-list-xl, fluid)
  167. v-layout(row, wrap, fill-height)
  168. v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
  169. v-card.radius-7.grey(flat, :class='$vuetify.dark ? `darken-3-d5` : `lighten-3`', height='100%')
  170. v-card-text
  171. .subheading(v-html='act.label')
  172. .body-1.mt-2(v-html='act.hint')
  173. v-btn.mx-0.mt-3(
  174. @click='executeAction(target.key, act.handler)'
  175. outline
  176. :color='$vuetify.dark ? `blue` : `primary`'
  177. :disabled='runningAction'
  178. :loading='runningActionHandler === act.handler'
  179. ) Run
  180. </template>
  181. <script>
  182. import _ from 'lodash'
  183. import moment from 'moment'
  184. import momentDurationFormatSetup from 'moment-duration-format'
  185. import DurationPicker from '../common/duration-picker.vue'
  186. import { LoopingRhombusesSpinner } from 'epic-spinners'
  187. import statusQuery from 'gql/admin/storage/storage-query-status.gql'
  188. import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
  189. import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
  190. import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
  191. momentDurationFormatSetup(moment)
  192. export default {
  193. components: {
  194. DurationPicker,
  195. LoopingRhombusesSpinner
  196. },
  197. filters: {
  198. startCase(val) { return _.startCase(val) }
  199. },
  200. data() {
  201. return {
  202. runningAction: false,
  203. runningActionHandler: '',
  204. selectedTarget: '',
  205. target: {},
  206. targets: [],
  207. status: []
  208. }
  209. },
  210. computed: {
  211. activeTargets() {
  212. return _.filter(this.targets, 'isEnabled')
  213. }
  214. },
  215. watch: {
  216. selectedTarget(newValue, oldValue) {
  217. this.target = _.find(this.targets, ['key', newValue]) || {}
  218. },
  219. targets(newValue, oldValue) {
  220. this.selectedTarget = _.get(_.find(this.targets, ['isEnabled', true]), 'key', 'disk')
  221. }
  222. },
  223. methods: {
  224. async refresh() {
  225. await this.$apollo.queries.targets.refetch()
  226. this.$store.commit('showNotification', {
  227. message: 'List of storage targets has been refreshed.',
  228. style: 'success',
  229. icon: 'cached'
  230. })
  231. },
  232. async save() {
  233. this.$store.commit(`loadingStart`, 'admin-storage-savetargets')
  234. await this.$apollo.mutate({
  235. mutation: targetsSaveMutation,
  236. variables: {
  237. targets: this.targets.map(tgt => _.pick(tgt, [
  238. 'isEnabled',
  239. 'key',
  240. 'config',
  241. 'mode',
  242. 'syncInterval'
  243. ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
  244. }
  245. })
  246. this.$store.commit('showNotification', {
  247. message: 'Storage configuration saved successfully.',
  248. style: 'success',
  249. icon: 'check'
  250. })
  251. this.$store.commit(`loadingStop`, 'admin-storage-savetargets')
  252. },
  253. getDefaultSchedule(val) {
  254. return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
  255. },
  256. async executeAction(targetKey, handler) {
  257. this.$store.commit(`loadingStart`, 'admin-storage-executeaction')
  258. this.runningAction = true
  259. this.runningActionHandler = handler
  260. try {
  261. await this.$apollo.mutate({
  262. mutation: targetExecuteActionMutation,
  263. variables: {
  264. targetKey,
  265. handler
  266. }
  267. })
  268. this.$store.commit('showNotification', {
  269. message: 'Action completed.',
  270. style: 'success',
  271. icon: 'check'
  272. })
  273. } catch (err) {
  274. console.warn(err)
  275. }
  276. this.runningAction = false
  277. this.runningActionHandler = ''
  278. this.$store.commit(`loadingStop`, 'admin-storage-executeaction')
  279. }
  280. },
  281. apollo: {
  282. targets: {
  283. query: targetsQuery,
  284. fetchPolicy: 'network-only',
  285. update: (data) => _.cloneDeep(data.storage.targets).map(str => ({
  286. ...str,
  287. config: _.sortBy(str.config.map(cfg => ({
  288. ...cfg,
  289. value: JSON.parse(cfg.value)
  290. })), [t => t.value.order])
  291. })),
  292. watchLoading (isLoading) {
  293. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')
  294. }
  295. },
  296. status: {
  297. query: statusQuery,
  298. fetchPolicy: 'network-only',
  299. update: (data) => data.storage.status,
  300. watchLoading (isLoading) {
  301. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')
  302. },
  303. pollInterval: 3000
  304. }
  305. }
  306. }
  307. </script>
  308. <style lang='scss' scoped>
  309. .targetlogo {
  310. width: 250px;
  311. height: 85px;
  312. float:right;
  313. display: flex;
  314. justify-content: flex-end;
  315. align-items: center;
  316. img {
  317. max-width: 100%;
  318. max-height: 50px;
  319. }
  320. }
  321. </style>