2
0

admin-utilities-importv1.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <template lang='pug'>
  2. v-card
  3. v-toolbar(flat, color='primary', dark, dense)
  4. .subtitle-1 {{ $t('admin:utilities.importv1Title') }}
  5. v-card-text
  6. .text-center
  7. img.animated.fadeInUp.wait-p1s(src='/svg/icon-software.svg')
  8. .body-2 Import from Wiki.js 1.x
  9. v-divider.my-4
  10. .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
  11. v-checkbox(
  12. label='Content + Uploads'
  13. value='content'
  14. color='deep-orange darken-2'
  15. v-model='importFilters'
  16. hide-details
  17. )
  18. template(v-slot:label)
  19. strong.deep-orange--text.text--darken-2 Content + Uploads
  20. .pl-8(v-if='wantContent')
  21. v-radio-group(v-model='contentMode', hide-details)
  22. v-radio(
  23. value='git'
  24. color='primary'
  25. )
  26. template(v-slot:label)
  27. div
  28. span Import from Git Connection
  29. .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
  30. .pl-8.mt-5(v-if='needGit')
  31. v-row
  32. v-col(cols='8')
  33. v-select(
  34. label='Authentication Mode'
  35. :items='gitAuthModes'
  36. v-model='gitAuthMode'
  37. outlined
  38. hide-details
  39. )
  40. v-col(cols='4')
  41. v-switch(
  42. label='Verify SSL Certificate'
  43. v-model='gitVerifySSL'
  44. hide-details
  45. color='primary'
  46. )
  47. v-col(:cols='gitAuthMode === `ssh` ? 6 : 8')
  48. v-text-field(
  49. outlined
  50. label='Repository URL'
  51. :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
  52. hide-details
  53. v-model='gitRepoUrl'
  54. )
  55. v-col(v-if='gitAuthMode === `ssh`', cols='2')
  56. v-text-field(
  57. label='Port'
  58. placeholder='e.g. 22'
  59. v-model='gitRepoPort'
  60. outlined
  61. hide-details
  62. )
  63. v-col(cols='4')
  64. v-text-field(
  65. label='Branch'
  66. placeholder='e.g. master'
  67. v-model='gitRepoBranch'
  68. outlined
  69. hide-details
  70. )
  71. v-col(v-if='gitAuthMode === `ssh`', cols='12')
  72. v-textarea(
  73. outlined
  74. label='Private Key Contents'
  75. placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
  76. hide-details
  77. v-model='gitPrivKey'
  78. )
  79. template(v-else-if='gitAuthMode === `basic`')
  80. v-col(cols='6')
  81. v-text-field(
  82. label='Username'
  83. v-model='gitUsername'
  84. outlined
  85. hide-details
  86. )
  87. v-col(cols='6')
  88. v-text-field(
  89. type='password'
  90. label='Password / PAT'
  91. v-model='gitPassword'
  92. outlined
  93. hide-details
  94. )
  95. v-col(cols='6')
  96. v-text-field(
  97. label='Default Author Email'
  98. placeholder='e.g. name@company.com'
  99. v-model='gitUserEmail'
  100. outlined
  101. hide-details
  102. )
  103. v-col(cols='6')
  104. v-text-field(
  105. label='Default Author Name'
  106. placeholder='e.g. John Smith'
  107. v-model='gitUserName'
  108. outlined
  109. hide-details
  110. )
  111. v-col(cols='12')
  112. v-text-field(
  113. label='Local Repository Path'
  114. placeholder='e.g. ./data/repo'
  115. v-model='gitRepoPath'
  116. outlined
  117. hide-details
  118. )
  119. .caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value.
  120. v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)
  121. .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.
  122. .body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously.
  123. v-radio-group(v-model='contentMode', hide-details)
  124. v-divider
  125. v-radio.mt-3(
  126. value='disk'
  127. color='primary'
  128. )
  129. template(v-slot:label)
  130. div
  131. span Import from local folder
  132. .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
  133. .pl-8.mt-5(v-if='needDisk')
  134. v-text-field(
  135. outlined
  136. label='Content Repo Path'
  137. hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
  138. persistent-hint
  139. v-model='contentPath'
  140. )
  141. v-checkbox(
  142. label='Users'
  143. value='users'
  144. color='deep-orange darken-2'
  145. v-model='importFilters'
  146. hide-details
  147. )
  148. template(v-slot:label)
  149. strong.deep-orange--text.text--darken-2 Users
  150. .pl-8.mt-5(v-if='wantUsers')
  151. v-text-field(
  152. outlined
  153. label='MongoDB Connection String'
  154. hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
  155. persistent-hint
  156. v-model='dbConnStr'
  157. )
  158. v-radio-group(v-model='groupMode', hide-details, mandatory)
  159. v-radio(
  160. value='MULTI'
  161. color='primary'
  162. )
  163. template(v-slot:label)
  164. div
  165. span Create groups for each unique user permissions configuration
  166. .caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created.
  167. v-divider
  168. v-radio.mt-3(
  169. value='SINGLE'
  170. color='primary'
  171. )
  172. template(v-slot:label)
  173. div
  174. span Create a single group with all imported users
  175. .caption: em The new group will have read permissions enabled by default.
  176. v-divider
  177. v-radio.mt-3(
  178. value='NONE'
  179. color='primary'
  180. )
  181. template(v-slot:label)
  182. div
  183. span Don't create any group
  184. .caption: em Users will not be able to access your wiki until they are assigned to a group.
  185. v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)
  186. .body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion.
  187. .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.
  188. v-card-chin
  189. v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
  190. v-icon(left, color='white') mdi-database-import
  191. span.white--text Start Import
  192. v-dialog(
  193. v-model='isLoading'
  194. persistent
  195. max-width='350'
  196. )
  197. v-card(color='deep-orange darken-2', dark)
  198. v-card-text.pa-10.text-center
  199. semipolar-spinner.animated.fadeIn(
  200. :animation-duration='1500'
  201. :size='65'
  202. color='#FFF'
  203. style='margin: 0 auto;'
  204. )
  205. .mt-5.body-1.white--text Importing from Wiki.js 1.x...
  206. .caption Please wait
  207. v-progress-linear.mt-5(
  208. color='white'
  209. :value='progress'
  210. stream
  211. rounded
  212. :buffer-value='0'
  213. )
  214. v-dialog(
  215. v-model='isSuccess'
  216. persistent
  217. max-width='350'
  218. )
  219. v-card(color='green darken-2', dark)
  220. v-card-text.pa-10.text-center
  221. v-icon(size='60') mdi-check-circle-outline
  222. .my-5.body-1.white--text Import completed
  223. template(v-if='wantUsers')
  224. .body-2
  225. span #[strong {{successUsers}}] users imported
  226. v-btn.text-none.ml-3(
  227. v-if='failedUsers.length > 0'
  228. text
  229. color='white'
  230. dark
  231. @click='showFailedUsers = true'
  232. )
  233. v-icon(left) mdi-alert
  234. span {{failedUsers.length}} failed
  235. .body-2 #[strong {{successGroups}}] groups created
  236. v-card-actions.green.darken-1
  237. v-spacer
  238. v-btn.px-5(
  239. color='white'
  240. outlined
  241. @click='isSuccess = false'
  242. ) Close
  243. v-spacer
  244. v-dialog(
  245. v-model='showFailedUsers'
  246. persistent
  247. max-width='800'
  248. )
  249. v-card(color='red darken-2', dark)
  250. v-toolbar(color='red darken-2', dense)
  251. v-icon mdi-alert
  252. .body-2.pl-3 Failed User Imports
  253. v-spacer
  254. v-btn.px-5(
  255. color='white'
  256. text
  257. @click='showFailedUsers = false'
  258. ) Close
  259. v-simple-table(dense, fixed-header, height='300px')
  260. template(v-slot:default)
  261. thead
  262. tr
  263. th Provider
  264. th Email
  265. th Error
  266. tbody
  267. tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
  268. td {{fusr.provider}}
  269. td {{fusr.email}}
  270. td {{fusr.error}}
  271. </template>
  272. <script>
  273. import _ from 'lodash'
  274. import { SemipolarSpinner } from 'epic-spinners'
  275. import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
  276. import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'
  277. import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'
  278. import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
  279. import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
  280. export default {
  281. components: {
  282. SemipolarSpinner
  283. },
  284. data() {
  285. return {
  286. importFilters: ['content', 'users'],
  287. groupMode: 'MULTI',
  288. contentMode: 'git',
  289. dbConnStr: 'mongodb://',
  290. contentPath: '/wiki-v1/repo',
  291. isLoading: false,
  292. isSuccess: false,
  293. gitAuthMode: 'ssh',
  294. gitAuthModes: [
  295. { text: 'SSH', value: 'ssh' },
  296. { text: 'Basic', value: 'basic' }
  297. ],
  298. gitVerifySSL: true,
  299. gitRepoUrl: '',
  300. gitRepoPort: 22,
  301. gitRepoBranch: 'master',
  302. gitPrivKey: '',
  303. gitUsername: '',
  304. gitPassword: '',
  305. gitUserEmail: '',
  306. gitUserName: '',
  307. gitRepoPath: './data/repo',
  308. progress: 0,
  309. successGroups: 0,
  310. successUsers: 0,
  311. successPages: 0,
  312. showFailedUsers: false,
  313. failedUsers: []
  314. }
  315. },
  316. computed: {
  317. wantContent () {
  318. return this.importFilters.indexOf('content') >= 0
  319. },
  320. wantUsers () {
  321. return this.importFilters.indexOf('users') >= 0
  322. },
  323. needDisk () {
  324. return this.contentMode === `disk`
  325. },
  326. needGit () {
  327. return this.contentMode === `git`
  328. }
  329. },
  330. methods: {
  331. async startImport () {
  332. this.isLoading = true
  333. this.progress = 0
  334. this.failedUsers = []
  335. _.delay(async () => {
  336. // -> Import Users
  337. if (this.wantUsers) {
  338. try {
  339. const resp = await this.$apollo.mutate({
  340. mutation: utilityImportv1UsersMutation,
  341. variables: {
  342. mongoDbConnString: this.dbConnStr,
  343. groupMode: this.groupMode
  344. }
  345. })
  346. const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
  347. if (!_.get(respObj, 'responseResult.succeeded', false)) {
  348. throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))
  349. }
  350. this.successUsers = _.get(respObj, 'usersCount', 0)
  351. this.successGroups = _.get(respObj, 'groupsCount', 0)
  352. this.failedUsers = _.get(respObj, 'failed', [])
  353. this.progress += 50
  354. } catch (err) {
  355. this.$store.commit('pushGraphError', err)
  356. this.isLoading = false
  357. return
  358. }
  359. }
  360. // -> Import Content
  361. if (this.wantContent) {
  362. try {
  363. const resp = await this.$apollo.query({
  364. query: storageTargetsQuery,
  365. fetchPolicy: 'network-only'
  366. })
  367. if (_.has(resp, 'data.storage.targets')) {
  368. this.progress += 10
  369. let targets = resp.data.storage.targets.map(str => {
  370. let nStr = {
  371. ...str,
  372. config: _.sortBy(str.config.map(cfg => ({
  373. ...cfg,
  374. value: JSON.parse(cfg.value)
  375. })), [t => t.value.order])
  376. }
  377. // -> Setup Git Module
  378. if (this.contentMode === 'git' && nStr.key === 'git') {
  379. nStr.isEnabled = true
  380. nStr.mode = 'sync'
  381. nStr.syncInterval = 'PT5M'
  382. nStr.config = [
  383. { key: 'authType', value: { value: this.gitAuthMode } },
  384. { key: 'repoUrl', value: { value: this.gitRepoUrl } },
  385. { key: 'branch', value: { value: this.gitRepoBranch } },
  386. { key: 'sshPort', value: { value: this.gitRepoPort } },
  387. { key: 'sshPrivateKeyMode', value: { value: 'contents' } },
  388. { key: 'sshPrivateKeyPath', value: { value: '' } },
  389. { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },
  390. { key: 'verifySSL', value: { value: this.gitVerifySSL } },
  391. { key: 'basicUsername', value: { value: this.gitUsername } },
  392. { key: 'basicPassword', value: { value: this.gitPassword } },
  393. { key: 'defaultEmail', value: { value: this.gitUserEmail } },
  394. { key: 'defaultName', value: { value: this.gitUserName } },
  395. { key: 'localRepoPath', value: { value: this.gitRepoPath } },
  396. { key: 'gitBinaryPath', value: { value: '' } }
  397. ]
  398. }
  399. // -> Setup Disk Module
  400. if (this.contentMode === 'disk' && nStr.key === 'disk') {
  401. nStr.isEnabled = true
  402. nStr.mode = 'push'
  403. nStr.syncInterval = 'P0D'
  404. nStr.config = [
  405. { key: 'path', value: { value: this.contentPath } },
  406. { key: 'createDailyBackups', value: { value: false } }
  407. ]
  408. }
  409. return nStr
  410. })
  411. // -> Save storage modules configuration
  412. const respSv = await this.$apollo.mutate({
  413. mutation: targetsSaveMutation,
  414. variables: {
  415. targets: targets.map(tgt => _.pick(tgt, [
  416. 'isEnabled',
  417. 'key',
  418. 'config',
  419. 'mode',
  420. 'syncInterval'
  421. ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
  422. }
  423. })
  424. const respObj = _.get(respSv, 'data.storage.updateTargets', {})
  425. if (!_.get(respObj, 'responseResult.succeeded', false)) {
  426. throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))
  427. }
  428. this.progress += 10
  429. // -> Wait for success sync
  430. let statusAttempts = 0
  431. while (statusAttempts < 10) {
  432. statusAttempts++
  433. const respStatus = await this.$apollo.query({
  434. query: storageStatusQuery,
  435. fetchPolicy: 'network-only'
  436. })
  437. if (_.has(respStatus, 'data.storage.status[0]')) {
  438. const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])
  439. if (!st) {
  440. throw new Error('Storage target could not be configured.')
  441. }
  442. switch (st.status) {
  443. case 'pending':
  444. if (statusAttempts >= 10) {
  445. throw new Error('Storage target is stuck in pending state. Try again.')
  446. } else {
  447. continue
  448. }
  449. case 'operational':
  450. statusAttempts = 10
  451. break
  452. case 'error':
  453. throw new Error(st.message)
  454. }
  455. } else {
  456. throw new Error('Failed to fetch storage sync status.')
  457. }
  458. }
  459. this.progress += 15
  460. // -> Perform import all
  461. const respImport = await this.$apollo.mutate({
  462. mutation: targetExecuteActionMutation,
  463. variables: {
  464. targetKey: this.contentMode,
  465. handler: 'importAll'
  466. }
  467. })
  468. const respImportObj = _.get(respImport, 'data.storage.executeAction', {})
  469. if (!_.get(respImportObj, 'responseResult.succeeded', false)) {
  470. throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occured'))
  471. }
  472. this.progress += 15
  473. } else {
  474. throw new Error('Failed to fetch storage targets.')
  475. }
  476. } catch (err) {
  477. this.$store.commit('pushGraphError', err)
  478. this.isLoading = false
  479. return
  480. }
  481. }
  482. this.isLoading = false
  483. this.isSuccess = true
  484. }, 1500)
  485. }
  486. }
  487. }
  488. </script>
  489. <style lang='scss'>
  490. </style>