admin-storage.vue 14 KB

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