admin-storage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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(src='/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text Storage
  9. .subheading.grey--text Set backup and sync targets for your content
  10. v-spacer
  11. v-btn(outline, color='grey', @click='refresh', large)
  12. v-icon refresh
  13. v-btn(color='success', @click='save', depressed, large)
  14. v-icon(left) check
  15. span {{$t('common:actions.apply')}}
  16. v-card.mt-3
  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 === `error`',
  56. extension-height='100'
  57. )
  58. .pa-3.red.darken-2.radius-7(v-if='tgt.status === `error`', slot='extension') {{tgt.message}}
  59. .body-2 {{tgt.title}}
  60. v-spacer
  61. .body-1 {{tgt.status}}
  62. v-alert.mt-3.radius-7(v-if='status.length < 1', outline, :value='true', color='indigo') You don't have any active storage target.
  63. v-tab-item(v-for='(tgt, n) in activeTargets', :key='tgt.key', :transition='false', :reverse-transition='false')
  64. v-card.wiki-form.pa-3(flat, tile)
  65. v-form
  66. .targetlogo
  67. img(:src='tgt.logo', :alt='tgt.title')
  68. v-subheader.pl-0 {{tgt.title}}
  69. .caption {{tgt.description}}
  70. .caption: a(:href='tgt.website') {{tgt.website}}
  71. v-divider.mt-3
  72. v-subheader.pl-0 Target Configuration
  73. .body-1.ml-3(v-if='!tgt.config || tgt.config.length < 1') This storage target has no configuration options you can modify.
  74. template(v-else, v-for='cfg in tgt.config')
  75. v-select(
  76. v-if='cfg.value.type === "string" && cfg.value.enum'
  77. outline
  78. background-color='grey lighten-2'
  79. :items='cfg.value.enum'
  80. :key='cfg.key'
  81. :label='cfg.value.title'
  82. v-model='cfg.value.value'
  83. prepend-icon='settings_applications'
  84. :hint='cfg.value.hint ? cfg.value.hint : ""'
  85. persistent-hint
  86. :class='cfg.value.hint ? "mb-2" : ""'
  87. )
  88. v-switch.mb-3(
  89. v-else-if='cfg.value.type === "boolean"'
  90. :key='cfg.key'
  91. :label='cfg.value.title'
  92. v-model='cfg.value.value'
  93. color='primary'
  94. prepend-icon='settings_applications'
  95. :hint='cfg.value.hint ? cfg.value.hint : ""'
  96. persistent-hint
  97. )
  98. v-text-field(
  99. v-else
  100. outline
  101. background-color='grey lighten-2'
  102. :key='cfg.key'
  103. :label='cfg.value.title'
  104. v-model='cfg.value.value'
  105. prepend-icon='settings_applications'
  106. :hint='cfg.value.hint ? cfg.value.hint : ""'
  107. persistent-hint
  108. :class='cfg.value.hint ? "mb-2" : ""'
  109. )
  110. v-divider.mt-3
  111. v-subheader.pl-0 Sync Direction
  112. .body-1.ml-3 Choose how content synchronization is handled for this storage target.
  113. .pr-3.pt-3
  114. v-radio-group.ml-3.py-0(v-model='tgt.mode')
  115. v-radio(
  116. label='Bi-directional'
  117. color='primary'
  118. value='sync'
  119. :disabled='tgt.supportedModes.indexOf(`sync`) < 0'
  120. )
  121. v-radio(
  122. label='Push to target'
  123. color='primary'
  124. value='push'
  125. :disabled='tgt.supportedModes.indexOf(`push`) < 0'
  126. )
  127. v-radio(
  128. label='Pull from target'
  129. color='primary'
  130. value='pull'
  131. :disabled='tgt.supportedModes.indexOf(`pull`) < 0'
  132. )
  133. .body-1.ml-3
  134. strong Bi-directional #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`sync`) < 0') Unsupported]
  135. .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.
  136. strong Push to target #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`push`) < 0') Unsupported]
  137. .pb-3 Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.
  138. strong Pull from target #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`pull`) < 0') Unsupported]
  139. .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!
  140. template(v-if='tgt.hasSchedule')
  141. v-divider.mt-3
  142. v-subheader.pl-0 Sync Schedule
  143. .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.
  144. .pa-3
  145. duration-picker(v-model='tgt.syncInterval')
  146. .caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(tgt.syncInterval)}}].
  147. .caption The default is every #[strong {{getDefaultSchedule(tgt.syncIntervalDefault)}}].
  148. </template>
  149. <script>
  150. import _ from 'lodash'
  151. import moment from 'moment'
  152. import momentDurationFormatSetup from 'moment-duration-format'
  153. import DurationPicker from '../common/duration-picker.vue'
  154. import { LoopingRhombusesSpinner } from 'epic-spinners'
  155. import statusQuery from 'gql/admin/storage/storage-query-status.gql'
  156. import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
  157. import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
  158. momentDurationFormatSetup(moment)
  159. export default {
  160. components: {
  161. DurationPicker,
  162. LoopingRhombusesSpinner
  163. },
  164. filters: {
  165. startCase(val) { return _.startCase(val) }
  166. },
  167. data() {
  168. return {
  169. currentTab: 0,
  170. targets: [],
  171. status: []
  172. }
  173. },
  174. computed: {
  175. activeTargets() {
  176. return _.filter(this.targets, 'isEnabled')
  177. }
  178. },
  179. methods: {
  180. async refresh() {
  181. await this.$apollo.queries.targets.refetch()
  182. this.$store.commit('showNotification', {
  183. message: 'List of storage targets has been refreshed.',
  184. style: 'success',
  185. icon: 'cached'
  186. })
  187. },
  188. async save() {
  189. this.$store.commit(`loadingStart`, 'admin-storage-savetargets')
  190. await this.$apollo.mutate({
  191. mutation: targetsSaveMutation,
  192. variables: {
  193. targets: this.targets.map(tgt => _.pick(tgt, [
  194. 'isEnabled',
  195. 'key',
  196. 'config',
  197. 'mode',
  198. 'syncInterval'
  199. ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
  200. }
  201. })
  202. this.currentTab = 0
  203. this.$store.commit('showNotification', {
  204. message: 'Storage configuration saved successfully.',
  205. style: 'success',
  206. icon: 'check'
  207. })
  208. this.$store.commit(`loadingStop`, 'admin-storage-savetargets')
  209. },
  210. getStatusColor(state) {
  211. switch (state) {
  212. case 'pending':
  213. return 'purple lighten-2'
  214. case 'operational':
  215. return 'green'
  216. case 'error':
  217. return 'red'
  218. default:
  219. return 'grey darken-2'
  220. }
  221. },
  222. getDefaultSchedule(val) {
  223. return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
  224. }
  225. },
  226. apollo: {
  227. targets: {
  228. query: targetsQuery,
  229. fetchPolicy: 'network-only',
  230. update: (data) => _.cloneDeep(data.storage.targets).map(str => ({
  231. ...str,
  232. config: _.sortBy(str.config.map(cfg => ({
  233. ...cfg,
  234. value: JSON.parse(cfg.value)
  235. })), [t => t.value.order])
  236. })),
  237. watchLoading (isLoading) {
  238. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')
  239. }
  240. },
  241. status: {
  242. query: statusQuery,
  243. fetchPolicy: 'network-only',
  244. update: (data) => data.storage.status,
  245. watchLoading (isLoading) {
  246. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')
  247. },
  248. pollInterval: 3000
  249. }
  250. }
  251. }
  252. </script>
  253. <style lang='scss' scoped>
  254. .targetlogo {
  255. width: 250px;
  256. height: 85px;
  257. float:right;
  258. display: flex;
  259. justify-content: flex-end;
  260. align-items: center;
  261. img {
  262. max-width: 100%;
  263. max-height: 50px;
  264. }
  265. }
  266. </style>