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.

372 lines
16 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='/_assets/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{$t('admin:storage.title')}}
  9. .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('admin:storage.subtitle')}}
  10. v-spacer
  11. v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/storage', target='_blank')
  12. v-icon mdi-help-circle
  13. v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
  14. v-icon mdi-refresh
  15. v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
  16. v-icon(left) mdi-check
  17. span {{$t('common:actions.apply')}}
  18. v-flex(lg3, xs12)
  19. v-card.animated.fadeInUp
  20. v-toolbar(flat, color='primary', dark, dense)
  21. .subtitle-1 {{$t('admin:storage.targets')}}
  22. v-list(two-line, dense).py-0
  23. template(v-for='(tgt, idx) in targets')
  24. v-list-item(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')
  25. v-list-item-avatar(size='24')
  26. v-icon(color='grey', v-if='!tgt.isAvailable') mdi-minus-box-outline
  27. v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') mdi-checkbox-marked-outline
  28. v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') mdi-checkbox-blank-outline
  29. v-list-item-content
  30. v-list-item-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}
  31. v-list-item-subtitle: .caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}
  32. v-list-item-avatar(v-if='selectedTarget === tgt.key', size='24')
  33. v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
  34. v-divider(v-if='idx < targets.length - 1')
  35. v-card.mt-3.animated.fadeInUp.wait-p2s
  36. v-toolbar(flat, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)
  37. .subtitle-1 {{$t('admin:storage.status')}}
  38. v-spacer
  39. looping-rhombuses-spinner(
  40. :animation-duration='5000'
  41. :rhombus-size='10'
  42. color='#FFF'
  43. )
  44. v-list.py-0(two-line, dense)
  45. template(v-for='(tgt, n) in status')
  46. v-list-item(:key='tgt.key')
  47. template(v-if='tgt.status === `pending`')
  48. v-list-item-avatar(color='purple')
  49. v-icon(color='white') mdi-clock-outline
  50. v-list-item-content
  51. v-list-item-title.body-2 {{tgt.title}}
  52. v-list-item-subtitle.purple--text.caption {{tgt.status}}
  53. v-list-item-action
  54. v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
  55. template(v-else-if='tgt.status === `operational`')
  56. v-list-item-avatar(color='green')
  57. v-icon(color='white') mdi-check-circle
  58. v-list-item-content
  59. v-list-item-title.body-2 {{tgt.title}}
  60. v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
  61. template(v-else)
  62. v-list-item-avatar(color='red')
  63. v-icon(color='white') mdi-close-circle-outline
  64. v-list-item-content
  65. v-list-item-title.body-2 {{tgt.title}}
  66. v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
  67. v-list-item-action
  68. v-menu
  69. template(v-slot:activator='{ on }')
  70. v-btn(icon, v-on='on')
  71. v-icon(color='red') mdi-information
  72. v-card(width='450')
  73. v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
  74. v-card-text {{tgt.message}}
  75. v-divider(v-if='n < status.length - 1')
  76. v-list-item(v-if='status.length < 1')
  77. em {{$t('admin:storage.noTarget')}}
  78. v-flex(xs12, lg9)
  79. v-card.wiki-form.animated.fadeInUp.wait-p2s
  80. v-toolbar(color='primary', dense, flat, dark)
  81. .subtitle-1 {{target.title}}
  82. v-spacer
  83. v-switch(
  84. dark
  85. color='blue lighten-5'
  86. label='Active'
  87. v-model='target.isEnabled'
  88. hide-details
  89. inset
  90. )
  91. v-card-info(color='blue')
  92. div
  93. div {{target.description}}
  94. span.caption: a(:href='target.website') {{target.website}}
  95. v-spacer
  96. .admin-providerlogo
  97. img(:src='target.logo', :alt='target.title')
  98. v-card-text
  99. v-form
  100. i18next.body-2(path='admin:storage.targetState', tag='div', v-if='target.isEnabled')
  101. v-chip(color='green', small, dark, label, place='state') {{$t('admin:storage.targetStateActive')}}
  102. i18next.body-2(path='admin:storage.targetState', tag='div', v-else)
  103. v-chip(color='red', small, dark, label, place='state') {{$t('admin:storage.targetStateInactive')}}
  104. v-divider.mt-3
  105. .overline.my-5 {{$t('admin:storage.targetConfig')}}
  106. .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
  107. template(v-else, v-for='cfg in target.config')
  108. v-select(
  109. v-if='cfg.value.type === "string" && cfg.value.enum'
  110. outlined
  111. :items='cfg.value.enum'
  112. :key='cfg.key'
  113. :label='cfg.value.title'
  114. v-model='cfg.value.value'
  115. prepend-icon='mdi-cog-box'
  116. :hint='cfg.value.hint ? cfg.value.hint : ""'
  117. persistent-hint
  118. :class='cfg.value.hint ? "mb-2" : ""'
  119. )
  120. v-switch.mb-3(
  121. v-else-if='cfg.value.type === "boolean"'
  122. :key='cfg.key'
  123. :label='cfg.value.title'
  124. v-model='cfg.value.value'
  125. color='primary'
  126. prepend-icon='mdi-cog-box'
  127. :hint='cfg.value.hint ? cfg.value.hint : ""'
  128. persistent-hint
  129. inset
  130. )
  131. v-textarea(
  132. v-else-if='cfg.value.type === "string" && cfg.value.multiline'
  133. outlined
  134. :key='cfg.key'
  135. :label='cfg.value.title'
  136. v-model='cfg.value.value'
  137. prepend-icon='mdi-cog-box'
  138. :hint='cfg.value.hint ? cfg.value.hint : ""'
  139. persistent-hint
  140. :class='cfg.value.hint ? "mb-2" : ""'
  141. )
  142. v-text-field(
  143. v-else
  144. outlined
  145. :key='cfg.key'
  146. :label='cfg.value.title'
  147. v-model='cfg.value.value'
  148. prepend-icon='mdi-cog-box'
  149. :hint='cfg.value.hint ? cfg.value.hint : ""'
  150. persistent-hint
  151. :class='cfg.value.hint ? "mb-2" : ""'
  152. )
  153. v-divider.mt-3
  154. .overline.my-5 {{$t('admin:storage.syncDirection')}}
  155. .body-2.ml-3 {{$t('admin:storage.syncDirectionSubtitle')}}
  156. .pr-3.pt-3
  157. v-radio-group.ml-3.py-0(v-model='target.mode')
  158. v-radio(
  159. :label='$t(`admin:storage.syncDirBi`)'
  160. color='primary'
  161. value='sync'
  162. :disabled='target.supportedModes.indexOf(`sync`) < 0'
  163. )
  164. v-radio(
  165. :label='$t(`admin:storage.syncDirPush`)'
  166. color='primary'
  167. value='push'
  168. :disabled='target.supportedModes.indexOf(`push`) < 0'
  169. )
  170. v-radio(
  171. :label='$t(`admin:storage.syncDirPull`)'
  172. color='primary'
  173. value='pull'
  174. :disabled='target.supportedModes.indexOf(`pull`) < 0'
  175. )
  176. .body-2.ml-3
  177. strong {{$t('admin:storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{$t('admin:storage.unsupported')}}]
  178. .pb-3 {{$t('admin:storage.syncDirBiHint')}}
  179. strong {{$t('admin:storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{$t('admin:storage.unsupported')}}]
  180. .pb-3 {{$t('admin:storage.syncDirPushHint')}}
  181. strong {{$t('admin:storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{$t('admin:storage.unsupported')}}]
  182. .pb-3 {{$t('admin:storage.syncDirPullHint')}}
  183. template(v-if='target.hasSchedule')
  184. v-divider.mt-3
  185. .overline.my-5 {{$t('admin:storage.syncSchedule')}}
  186. .body-2.ml-3 {{$t('admin:storage.syncScheduleHint')}}
  187. .pa-3
  188. duration-picker(v-model='target.syncInterval')
  189. i18next.caption.mt-3(path='admin:storage.syncScheduleCurrent', tag='div')
  190. strong(place='schedule') {{getDefaultSchedule(target.syncInterval)}}
  191. i18next.caption(path='admin:storage.syncScheduleDefault', tag='div')
  192. strong(place='schedule') {{getDefaultSchedule(target.syncIntervalDefault)}}
  193. template(v-if='target.actions && target.actions.length > 0')
  194. v-divider.mt-3
  195. .overline.my-5 {{$t('admin:storage.actions')}}
  196. v-alert(outlined, :value='!target.isEnabled', color='red', icon='mdi-alert')
  197. .body-2 {{$t('admin:storage.actionsInactiveWarn')}}
  198. v-container.pt-0(grid-list-xl, fluid)
  199. v-layout(row, wrap, fill-height)
  200. v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
  201. v-card.radius-7.grey(flat, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`', height='100%')
  202. v-card-text
  203. .subtitle-1(v-html='act.label')
  204. .body-2.mt-4(v-html='act.hint')
  205. v-btn.mx-0.mt-5(
  206. @click='executeAction(target.key, act.handler)'
  207. outlined
  208. :color='$vuetify.theme.dark ? `blue` : `primary`'
  209. :disabled='runningAction || !target.isEnabled'
  210. :loading='runningActionHandler === act.handler'
  211. ) {{$t('admin:storage.actionRun')}}
  212. </template>
  213. <script>
  214. import _ from 'lodash'
  215. import moment from 'moment'
  216. import momentDurationFormatSetup from 'moment-duration-format'
  217. import DurationPicker from '../common/duration-picker.vue'
  218. import { LoopingRhombusesSpinner } from 'epic-spinners'
  219. import statusQuery from 'gql/admin/storage/storage-query-status.gql'
  220. import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
  221. import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
  222. import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
  223. momentDurationFormatSetup(moment)
  224. export default {
  225. components: {
  226. DurationPicker,
  227. LoopingRhombusesSpinner
  228. },
  229. filters: {
  230. startCase(val) { return _.startCase(val) }
  231. },
  232. data() {
  233. return {
  234. runningAction: false,
  235. runningActionHandler: '',
  236. selectedTarget: '',
  237. target: {
  238. supportedModes: []
  239. },
  240. targets: [],
  241. status: []
  242. }
  243. },
  244. computed: {
  245. activeTargets() {
  246. return _.filter(this.targets, 'isEnabled')
  247. }
  248. },
  249. watch: {
  250. selectedTarget(newValue, oldValue) {
  251. this.target = _.find(this.targets, ['key', newValue]) || {}
  252. },
  253. targets(newValue, oldValue) {
  254. this.selectedTarget = _.get(_.find(this.targets, ['isEnabled', true]), 'key', 'disk')
  255. }
  256. },
  257. methods: {
  258. async refresh() {
  259. await this.$apollo.queries.targets.refetch()
  260. this.$store.commit('showNotification', {
  261. message: 'List of storage targets has been refreshed.',
  262. style: 'success',
  263. icon: 'cached'
  264. })
  265. },
  266. async save() {
  267. this.$store.commit(`loadingStart`, 'admin-storage-savetargets')
  268. await this.$apollo.mutate({
  269. mutation: targetsSaveMutation,
  270. variables: {
  271. targets: this.targets.map(tgt => _.pick(tgt, [
  272. 'isEnabled',
  273. 'key',
  274. 'config',
  275. 'mode',
  276. 'syncInterval'
  277. ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
  278. }
  279. })
  280. this.$store.commit('showNotification', {
  281. message: 'Storage configuration saved successfully.',
  282. style: 'success',
  283. icon: 'check'
  284. })
  285. this.$store.commit(`loadingStop`, 'admin-storage-savetargets')
  286. },
  287. getDefaultSchedule(val) {
  288. if (!val) { return 'N/A' }
  289. return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
  290. },
  291. async executeAction(targetKey, handler) {
  292. this.$store.commit(`loadingStart`, 'admin-storage-executeaction')
  293. this.runningAction = true
  294. this.runningActionHandler = handler
  295. try {
  296. await this.$apollo.mutate({
  297. mutation: targetExecuteActionMutation,
  298. variables: {
  299. targetKey,
  300. handler
  301. }
  302. })
  303. this.$store.commit('showNotification', {
  304. message: 'Action completed.',
  305. style: 'success',
  306. icon: 'check'
  307. })
  308. } catch (err) {
  309. console.warn(err)
  310. }
  311. this.runningAction = false
  312. this.runningActionHandler = ''
  313. this.$store.commit(`loadingStop`, 'admin-storage-executeaction')
  314. }
  315. },
  316. apollo: {
  317. targets: {
  318. query: targetsQuery,
  319. fetchPolicy: 'network-only',
  320. update: (data) => _.cloneDeep(data.storage.targets).map(str => ({
  321. ...str,
  322. config: _.sortBy(str.config.map(cfg => ({
  323. ...cfg,
  324. value: JSON.parse(cfg.value)
  325. })), [t => t.value.order])
  326. })),
  327. watchLoading (isLoading) {
  328. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')
  329. }
  330. },
  331. status: {
  332. query: statusQuery,
  333. fetchPolicy: 'network-only',
  334. update: (data) => data.storage.status,
  335. watchLoading (isLoading) {
  336. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')
  337. },
  338. pollInterval: 3000
  339. }
  340. }
  341. }
  342. </script>
  343. <style lang='scss' scoped>
  344. .targetlogo {
  345. width: 250px;
  346. height: 85px;
  347. float:right;
  348. display: flex;
  349. justify-content: flex-end;
  350. align-items: center;
  351. img {
  352. max-width: 100%;
  353. max-height: 50px;
  354. }
  355. }
  356. </style>