|
|
|
<template lang='pug'>
|
|
|
|
v-card
|
|
|
|
v-toolbar(flat, color='primary', dark, dense)
|
|
|
|
.subtitle-1 {{ $t('admin:utilities.importv1Title') }}
|
|
|
|
v-card-text
|
|
|
|
.text-center
|
|
|
|
img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')
|
|
|
|
.body-2 Import from Wiki.js 1.x
|
|
|
|
v-divider.my-4
|
|
|
|
.body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
|
|
|
|
v-checkbox(
|
|
|
|
label='Content + Uploads'
|
|
|
|
value='content'
|
|
|
|
color='deep-orange darken-2'
|
|
|
|
v-model='importFilters'
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
strong.deep-orange--text.text--darken-2 Content + Uploads
|
|
|
|
.pl-8(v-if='wantContent')
|
|
|
|
v-radio-group(v-model='contentMode', hide-details)
|
|
|
|
v-radio(
|
|
|
|
value='git'
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
div
|
|
|
|
span Import from Git Connection
|
|
|
|
.caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
|
|
|
|
.pl-8.mt-5(v-if='needGit')
|
|
|
|
v-row
|
|
|
|
v-col(cols='8')
|
|
|
|
v-select(
|
|
|
|
label='Authentication Mode'
|
|
|
|
:items='gitAuthModes'
|
|
|
|
v-model='gitAuthMode'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(cols='4')
|
|
|
|
v-switch(
|
|
|
|
label='Verify SSL Certificate'
|
|
|
|
v-model='gitVerifySSL'
|
|
|
|
hide-details
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
v-col(cols='8')
|
|
|
|
v-text-field(
|
|
|
|
outlined
|
|
|
|
label='Repository URL'
|
|
|
|
:placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
|
|
|
|
hide-details
|
|
|
|
v-model='gitRepoUrl'
|
|
|
|
)
|
|
|
|
v-col(cols='4')
|
|
|
|
v-text-field(
|
|
|
|
label='Branch'
|
|
|
|
placeholder='e.g. master'
|
|
|
|
v-model='gitRepoBranch'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(v-if='gitAuthMode === `ssh`', cols='12')
|
|
|
|
v-textarea(
|
|
|
|
outlined
|
|
|
|
label='Private Key Contents'
|
|
|
|
placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
|
|
|
|
hide-details
|
|
|
|
v-model='gitPrivKey'
|
|
|
|
)
|
|
|
|
template(v-else-if='gitAuthMode === `basic`')
|
|
|
|
v-col(cols='6')
|
|
|
|
v-text-field(
|
|
|
|
label='Username'
|
|
|
|
v-model='gitUsername'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(cols='6')
|
|
|
|
v-text-field(
|
|
|
|
type='password'
|
|
|
|
label='Password / PAT'
|
|
|
|
v-model='gitPassword'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(cols='6')
|
|
|
|
v-text-field(
|
|
|
|
label='Default Author Email'
|
|
|
|
placeholder='e.g. name@company.com'
|
|
|
|
v-model='gitUserEmail'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(cols='6')
|
|
|
|
v-text-field(
|
|
|
|
label='Default Author Name'
|
|
|
|
placeholder='e.g. John Smith'
|
|
|
|
v-model='gitUserName'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
v-col(cols='12')
|
|
|
|
v-text-field(
|
|
|
|
label='Local Repository Path'
|
|
|
|
placeholder='e.g. ./data/repo'
|
|
|
|
v-model='gitRepoPath'
|
|
|
|
outlined
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
.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.
|
|
|
|
v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)
|
|
|
|
.body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.
|
|
|
|
.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.
|
|
|
|
v-radio-group(v-model='contentMode', hide-details)
|
|
|
|
v-divider
|
|
|
|
v-radio.mt-3(
|
|
|
|
value='disk'
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
div
|
|
|
|
span Import from local folder
|
|
|
|
.caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
|
|
|
|
.pl-8.mt-5(v-if='needDisk')
|
|
|
|
v-text-field(
|
|
|
|
outlined
|
|
|
|
label='Content Repo Path'
|
|
|
|
hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
|
|
|
|
persistent-hint
|
|
|
|
v-model='contentPath'
|
|
|
|
)
|
|
|
|
|
|
|
|
v-checkbox(
|
|
|
|
label='Users'
|
|
|
|
value='users'
|
|
|
|
color='deep-orange darken-2'
|
|
|
|
v-model='importFilters'
|
|
|
|
hide-details
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
strong.deep-orange--text.text--darken-2 Users
|
|
|
|
.pl-8.mt-5(v-if='wantUsers')
|
|
|
|
v-text-field(
|
|
|
|
outlined
|
|
|
|
label='MongoDB Connection String'
|
|
|
|
hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
|
|
|
|
persistent-hint
|
|
|
|
v-model='dbConnStr'
|
|
|
|
)
|
|
|
|
v-radio-group(v-model='groupMode', hide-details, mandatory)
|
|
|
|
v-radio(
|
|
|
|
value='MULTI'
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
div
|
|
|
|
span Create groups for each unique user permissions configuration
|
|
|
|
.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.
|
|
|
|
v-divider
|
|
|
|
v-radio.mt-3(
|
|
|
|
value='SINGLE'
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
div
|
|
|
|
span Create a single group with all imported users
|
|
|
|
.caption: em The new group will have read permissions enabled by default.
|
|
|
|
v-divider
|
|
|
|
v-radio.mt-3(
|
|
|
|
value='NONE'
|
|
|
|
color='primary'
|
|
|
|
)
|
|
|
|
template(v-slot:label)
|
|
|
|
div
|
|
|
|
span Don't create any group
|
|
|
|
.caption: em Users will not be able to access your wiki until they are assigned to a group.
|
|
|
|
|
|
|
|
v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)
|
|
|
|
.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.
|
|
|
|
.caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.
|
|
|
|
|
|
|
|
v-card-chin
|
|
|
|
v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
|
|
|
|
v-icon(left, color='white') mdi-database-import
|
|
|
|
span.white--text Start Import
|
|
|
|
v-dialog(
|
|
|
|
v-model='isLoading'
|
|
|
|
persistent
|
|
|
|
max-width='350'
|
|
|
|
)
|
|
|
|
v-card(color='deep-orange darken-2', dark)
|
|
|
|
v-card-text.pa-10.text-center
|
|
|
|
semipolar-spinner.animated.fadeIn(
|
|
|
|
:animation-duration='1500'
|
|
|
|
:size='65'
|
|
|
|
color='#FFF'
|
|
|
|
style='margin: 0 auto;'
|
|
|
|
)
|
|
|
|
.mt-5.body-1.white--text Importing from Wiki.js 1.x...
|
|
|
|
.caption Please wait
|
|
|
|
v-progress-linear.mt-5(
|
|
|
|
color='white'
|
|
|
|
:value='progress'
|
|
|
|
stream
|
|
|
|
rounded
|
|
|
|
:buffer-value='0'
|
|
|
|
)
|
|
|
|
v-dialog(
|
|
|
|
v-model='isSuccess'
|
|
|
|
persistent
|
|
|
|
max-width='350'
|
|
|
|
)
|
|
|
|
v-card(color='green darken-2', dark)
|
|
|
|
v-card-text.pa-10.text-center
|
|
|
|
v-icon(size='60') mdi-check-circle-outline
|
|
|
|
.my-5.body-1.white--text Import completed
|
|
|
|
template(v-if='wantUsers')
|
|
|
|
.body-2
|
|
|
|
span #[strong {{successUsers}}] users imported
|
|
|
|
v-btn.text-none.ml-3(
|
|
|
|
v-if='failedUsers.length > 0'
|
|
|
|
text
|
|
|
|
color='white'
|
|
|
|
dark
|
|
|
|
@click='showFailedUsers = true'
|
|
|
|
)
|
|
|
|
v-icon(left) mdi-alert
|
|
|
|
span {{failedUsers.length}} failed
|
|
|
|
.body-2 #[strong {{successGroups}}] groups created
|
|
|
|
v-card-actions.green.darken-1
|
|
|
|
v-spacer
|
|
|
|
v-btn.px-5(
|
|
|
|
color='white'
|
|
|
|
outlined
|
|
|
|
@click='isSuccess = false'
|
|
|
|
) Close
|
|
|
|
v-spacer
|
|
|
|
v-dialog(
|
|
|
|
v-model='showFailedUsers'
|
|
|
|
persistent
|
|
|
|
max-width='800'
|
|
|
|
)
|
|
|
|
v-card(color='red darken-2', dark)
|
|
|
|
v-toolbar(color='red darken-2', dense)
|
|
|
|
v-icon mdi-alert
|
|
|
|
.body-2.pl-3 Failed User Imports
|
|
|
|
v-spacer
|
|
|
|
v-btn.px-5(
|
|
|
|
color='white'
|
|
|
|
text
|
|
|
|
@click='showFailedUsers = false'
|
|
|
|
) Close
|
|
|
|
v-simple-table(dense, fixed-header, height='300px')
|
|
|
|
template(v-slot:default)
|
|
|
|
thead
|
|
|
|
tr
|
|
|
|
th Provider
|
|
|
|
th Email
|
|
|
|
th Error
|
|
|
|
tbody
|
|
|
|
tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
|
|
|
|
td {{fusr.provider}}
|
|
|
|
td {{fusr.email}}
|
|
|
|
td {{fusr.error}}
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import _ from 'lodash'
|
|
|
|
|
|
|
|
import { SemipolarSpinner } from 'epic-spinners'
|
|
|
|
|
|
|
|
import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
|
|
|
|
import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'
|
|
|
|
import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'
|
|
|
|
import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
|
|
|
|
import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
|
|
|
|
|
|
|
|
export default {
|
|
|
|
components: {
|
|
|
|
SemipolarSpinner
|
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
importFilters: ['content', 'users'],
|
|
|
|
groupMode: 'MULTI',
|
|
|
|
contentMode: 'git',
|
|
|
|
dbConnStr: 'mongodb://',
|
|
|
|
contentPath: '/wiki-v1/repo',
|
|
|
|
isLoading: false,
|
|
|
|
isSuccess: false,
|
|
|
|
gitAuthMode: 'ssh',
|
|
|
|
gitAuthModes: [
|
|
|
|
{ text: 'SSH', value: 'ssh' },
|
|
|
|
{ text: 'Basic', value: 'basic' }
|
|
|
|
],
|
|
|
|
gitVerifySSL: true,
|
|
|
|
gitRepoUrl: '',
|
|
|
|
gitRepoBranch: 'master',
|
|
|
|
gitPrivKey: '',
|
|
|
|
gitUsername: '',
|
|
|
|
gitPassword: '',
|
|
|
|
gitUserEmail: '',
|
|
|
|
gitUserName: '',
|
|
|
|
gitRepoPath: './data/repo',
|
|
|
|
progress: 0,
|
|
|
|
successGroups: 0,
|
|
|
|
successUsers: 0,
|
|
|
|
successPages: 0,
|
|
|
|
showFailedUsers: false,
|
|
|
|
failedUsers: []
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
wantContent () {
|
|
|
|
return this.importFilters.indexOf('content') >= 0
|
|
|
|
},
|
|
|
|
wantUsers () {
|
|
|
|
return this.importFilters.indexOf('users') >= 0
|
|
|
|
},
|
|
|
|
needDisk () {
|
|
|
|
return this.contentMode === `disk`
|
|
|
|
},
|
|
|
|
needGit () {
|
|
|
|
return this.contentMode === `git`
|
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
async startImport () {
|
|
|
|
this.isLoading = true
|
|
|
|
this.progress = 0
|
|
|
|
this.failedUsers = []
|
|
|
|
|
|
|
|
_.delay(async () => {
|
|
|
|
// -> Import Users
|
|
|
|
|
|
|
|
if (this.wantUsers) {
|
|
|
|
try {
|
|
|
|
const resp = await this.$apollo.mutate({
|
|
|
|
mutation: utilityImportv1UsersMutation,
|
|
|
|
variables: {
|
|
|
|
mongoDbConnString: this.dbConnStr,
|
|
|
|
groupMode: this.groupMode
|
|
|
|
}
|
|
|
|
})
|
|
|
|
const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
|
|
|
|
if (!_.get(respObj, 'responseResult.succeeded', false)) {
|
|
|
|
throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))
|
|
|
|
}
|
|
|
|
this.successUsers = _.get(respObj, 'usersCount', 0)
|
|
|
|
this.successGroups = _.get(respObj, 'groupsCount', 0)
|
|
|
|
this.failedUsers = _.get(respObj, 'failed', [])
|
|
|
|
this.progress += 50
|
|
|
|
} catch (err) {
|
|
|
|
this.$store.commit('pushGraphError', err)
|
|
|
|
this.isLoading = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// -> Import Content
|
|
|
|
|
|
|
|
if (this.wantContent) {
|
|
|
|
try {
|
|
|
|
const resp = await this.$apollo.query({
|
|
|
|
query: storageTargetsQuery,
|
|
|
|
fetchPolicy: 'network-only'
|
|
|
|
})
|
|
|
|
if (_.has(resp, 'data.storage.targets')) {
|
|
|
|
this.progress += 10
|
|
|
|
let targets = resp.data.storage.targets.map(str => {
|
|
|
|
let nStr = {
|
|
|
|
...str,
|
|
|
|
config: _.sortBy(str.config.map(cfg => ({
|
|
|
|
...cfg,
|
|
|
|
value: JSON.parse(cfg.value)
|
|
|
|
})), [t => t.value.order])
|
|
|
|
}
|
|
|
|
|
|
|
|
// -> Setup Git Module
|
|
|
|
|
|
|
|
if (this.contentMode === 'git' && nStr.key === 'git') {
|
|
|
|
nStr.isEnabled = true
|
|
|
|
nStr.mode = 'sync'
|
|
|
|
nStr.syncInterval = 'PT5M'
|
|
|
|
nStr.config = [
|
|
|
|
{ key: 'authType', value: { value: this.gitAuthMode } },
|
|
|
|
{ key: 'repoUrl', value: { value: this.gitRepoUrl } },
|
|
|
|
{ key: 'branch', value: { value: this.gitRepoBranch } },
|
|
|
|
{ key: 'sshPrivateKeyMode', value: { value: 'contents' } },
|
|
|
|
{ key: 'sshPrivateKeyPath', value: { value: '' } },
|
|
|
|
{ key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },
|
|
|
|
{ key: 'verifySSL', value: { value: this.gitVerifySSL } },
|
|
|
|
{ key: 'basicUsername', value: { value: this.gitUsername } },
|
|
|
|
{ key: 'basicPassword', value: { value: this.gitPassword } },
|
|
|
|
{ key: 'defaultEmail', value: { value: this.gitUserEmail } },
|
|
|
|
{ key: 'defaultName', value: { value: this.gitUserName } },
|
|
|
|
{ key: 'localRepoPath', value: { value: this.gitRepoPath } },
|
|
|
|
{ key: 'gitBinaryPath', value: { value: '' } }
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
// -> Setup Disk Module
|
|
|
|
if (this.contentMode === 'disk' && nStr.key === 'disk') {
|
|
|
|
nStr.isEnabled = true
|
|
|
|
nStr.mode = 'push'
|
|
|
|
nStr.syncInterval = 'P0D'
|
|
|
|
nStr.config = [
|
|
|
|
{ key: 'path', value: { value: this.contentPath } },
|
|
|
|
{ key: 'createDailyBackups', value: { value: false } }
|
|
|
|
]
|
|
|
|
}
|
|
|
|
return nStr
|
|
|
|
})
|
|
|
|
|
|
|
|
// -> Save storage modules configuration
|
|
|
|
|
|
|
|
const respSv = await this.$apollo.mutate({
|
|
|
|
mutation: targetsSaveMutation,
|
|
|
|
variables: {
|
|
|
|
targets: targets.map(tgt => _.pick(tgt, [
|
|
|
|
'isEnabled',
|
|
|
|
'key',
|
|
|
|
'config',
|
|
|
|
'mode',
|
|
|
|
'syncInterval'
|
|
|
|
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
const respObj = _.get(respSv, 'data.storage.updateTargets', {})
|
|
|
|
if (!_.get(respObj, 'responseResult.succeeded', false)) {
|
|
|
|
throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured'))
|
|
|
|
}
|
|
|
|
|
|
|
|
this.progress += 10
|
|
|
|
|
|
|
|
// -> Wait for success sync
|
|
|
|
|
|
|
|
let statusAttempts = 0
|
|
|
|
while (statusAttempts < 10) {
|
|
|
|
statusAttempts++
|
|
|
|
const respStatus = await this.$apollo.query({
|
|
|
|
query: storageStatusQuery,
|
|
|
|
fetchPolicy: 'network-only'
|
|
|
|
})
|
|
|
|
if (_.has(respStatus, 'data.storage.status[0]')) {
|
|
|
|
const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])
|
|
|
|
if (!st) {
|
|
|
|
throw new Error('Storage target could not be configured.')
|
|
|
|
}
|
|
|
|
switch (st.status) {
|
|
|
|
case 'pending':
|
|
|
|
if (statusAttempts >= 10) {
|
|
|
|
throw new Error('Storage target is stuck in pending state. Try again.')
|
|
|
|
} else {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
case 'operational':
|
|
|
|
statusAttempts = 10
|
|
|
|
break
|
|
|
|
case 'error':
|
|
|
|
throw new Error(st.message)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error('Failed to fetch storage sync status.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.progress += 15
|
|
|
|
|
|
|
|
// -> Perform import all
|
|
|
|
|
|
|
|
const respImport = await this.$apollo.mutate({
|
|
|
|
mutation: targetExecuteActionMutation,
|
|
|
|
variables: {
|
|
|
|
targetKey: this.contentMode,
|
|
|
|
handler: 'importAll'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const respImportObj = _.get(respImport, 'data.storage.executeAction', {})
|
|
|
|
if (!_.get(respImportObj, 'responseResult.succeeded', false)) {
|
|
|
|
throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occured'))
|
|
|
|
}
|
|
|
|
|
|
|
|
this.progress += 15
|
|
|
|
} else {
|
|
|
|
throw new Error('Failed to fetch storage targets.')
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
this.$store.commit('pushGraphError', err)
|
|
|
|
this.isLoading = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isLoading = false
|
|
|
|
this.isSuccess = true
|
|
|
|
}, 1500)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang='scss'>
|
|
|
|
|
|
|
|
</style>
|