feat: MigrateService script setup #2432
|
@ -31,8 +31,8 @@ import ListTeamsComponent from '../views/teams/ListTeams.vue'
|
|||
import ListLabelsComponent from '../views/labels/ListLabels.vue'
|
||||
import NewLabelComponent from '../views/labels/NewLabel.vue'
|
||||
// Migration
|
||||
import MigrationComponent from '../views/migrator/Migrate.vue'
|
||||
import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
|
||||
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
||||
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
||||
// List Views
|
||||
import ListList from '../views/list/ListList.vue'
|
||||
const ListGantt = () => import('../views/list/ListGantt.vue')
|
||||
|
@ -445,7 +445,11 @@ const router = createRouter({
|
|||
{
|
||||
path: '/migrate/:service',
|
||||
name: 'migrate.service',
|
||||
component: MigrateServiceComponent,
|
||||
component: MigrationHandlerComponent,
|
||||
props: route => ({
|
||||
service: route.params.service as string,
|
||||
code: route.params.code as string,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/filters/new',
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import AbstractService from '../abstractService'
|
||||
|
||||
export type MigrationConfig = { code: string }
|
||||
|
||||
// This service builds on top of the abstract service and basically just hides away method names.
|
||||
// It enables migration services to be created with minimal overhead and even better method names.
|
||||
export default class AbstractMigrationService extends AbstractService {
|
||||
export default class AbstractMigrationService extends AbstractService<MigrationConfig> {
|
||||
serviceUrlKey = ''
|
||||
|
||||
constructor(serviceUrlKey) {
|
||||
constructor(serviceUrlKey: string) {
|
||||
super({
|
||||
update: '/migration/' + serviceUrlKey + '/migrate',
|
||||
})
|
||||
|
@ -20,7 +22,7 @@ export default class AbstractMigrationService extends AbstractService {
|
|||
return this.getM('/migration/' + this.serviceUrlKey + '/status')
|
||||
}
|
||||
|
||||
migrate(data) {
|
||||
migrate(data: MigrationConfig) {
|
||||
return this.update(data)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import type {IFile} from '@/modelTypes/IFile'
|
||||
import AbstractService from '../abstractService'
|
||||
|
||||
// This service builds on top of the abstract service and basically just hides away method names.
|
||||
|
@ -21,7 +20,7 @@ export default class AbstractMigrationFileService extends AbstractService {
|
|||
return false
|
||||
}
|
||||
|
||||
migrate(file: IFile) {
|
||||
migrate(file: File) {
|
||||
return this.uploadFile(
|
||||
this.paths.create,
|
||||
file,
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
type="file"
|
||||
/>
|
||||
<x-button
|
||||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading || undefined"
|
||||
@click="$refs.uploadInput.click()"
|
||||
:loading="migrationFileService.loading"
|
||||
:disabled="migrationFileService.loading || undefined"
|
||||
@click="uploadInput?.click()"
|
||||
>
|
||||
{{ $t('migrate.upload') }}
|
||||
</x-button>
|
||||
|
@ -57,129 +57,118 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<message class="mb-4">
|
||||
<Message class="mb-4">
|
||||
{{ message }}
|
||||
</message>
|
||||
</Message>
|
||||
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
|
||||
import AbstractMigrationService from '@/services/migrator/abstractMigration'
|
||||
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
|
||||
import Logo from '@/assets/logo.svg?component'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import { setTitle } from '@/helpers/setTitle'
|
||||
|
||||
import {formatDateLong} from '@/helpers/time/formatDate'
|
||||
|
||||
import {MIGRATORS} from './migrators'
|
||||
import { useNamespaceStore } from '@/stores/namespaces'
|
||||
|
||||
const PROGRESS_DOTS_COUNT = 8
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MigrateService',
|
||||
|
||||
components: {
|
||||
Logo,
|
||||
Message,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
progressDotsCount: PROGRESS_DOTS_COUNT,
|
||||
authUrl: '',
|
||||
isMigrating: false,
|
||||
lastMigrationDate: null,
|
||||
message: '',
|
||||
migratorAuthCode: '',
|
||||
migrationService: null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
migrator() {
|
||||
return MIGRATORS[this.$route.params.service]
|
||||
},
|
||||
},
|
||||
|
||||
export default {
|
||||
beforeRouteEnter(to) {
|
||||
if (MIGRATORS[to.params.service] === undefined) {
|
||||
if (MIGRATORS[to.params.service as string] === undefined) {
|
||||
return {name: 'not-found'}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
created() {
|
||||
this.initMigration()
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
mounted() {
|
||||
setTitle(this.$t('migrate.titleService', {name: this.migrator.name}))
|
||||
},
|
||||
import Logo from '@/assets/logo.svg?component'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
||||
methods: {
|
||||
formatDateLong,
|
||||
import AbstractMigrationService, { type MigrationConfig } from '@/services/migrator/abstractMigration'
|
||||
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
|
||||
|
||||
async initMigration() {
|
||||
this.migrationService = this.migrator.isFileMigrator
|
||||
? new AbstractMigrationFileService(this.migrator.id)
|
||||
: new AbstractMigrationService(this.migrator.id)
|
||||
import {formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
|
||||
if (this.migrator.isFileMigrator) {
|
||||
return
|
||||
}
|
||||
import {MIGRATORS} from './migrators'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
this.authUrl = await this.migrationService.getAuthUrl().then(({url}) => url)
|
||||
const PROGRESS_DOTS_COUNT = 8
|
||||
|
||||
this.migratorAuthCode = location.hash.startsWith('#token=')
|
||||
? location.hash.substring(7)
|
||||
: this.$route.query.code
|
||||
const props = defineProps<{
|
||||
service: string,
|
||||
code?: string,
|
||||
}>()
|
||||
|
||||
if (!this.migratorAuthCode) {
|
||||
return
|
||||
}
|
||||
const {time} = await this.migrationService.getStatus()
|
||||
if (time) {
|
||||
this.lastMigrationDate = typeof time === 'string' && time?.startsWith('0001-')
|
||||
? null
|
||||
: new Date(time)
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
if (this.lastMigrationDate) {
|
||||
return
|
||||
}
|
||||
}
|
||||
await this.migrate()
|
||||
},
|
||||
const progressDotsCount = ref(PROGRESS_DOTS_COUNT)
|
||||
const authUrl = ref('')
|
||||
const isMigrating = ref(false)
|
||||
const lastMigrationDate = ref<Date | null>(null)
|
||||
const message = ref('')
|
||||
const migratorAuthCode = ref('')
|
||||
|
||||
async migrate() {
|
||||
this.isMigrating = true
|
||||
this.lastMigrationDate = null
|
||||
this.message = ''
|
||||
const migrator = computed(() => MIGRATORS[props.service])
|
||||
|
||||
let migrationConfig = {code: this.migratorAuthCode}
|
||||
const migrationService = shallowReactive(new AbstractMigrationService(migrator.value.id))
|
||||
const migrationFileService = shallowReactive(new AbstractMigrationFileService(migrator.value.id))
|
||||
|
||||
if (this.migrator.isFileMigrator) {
|
||||
if (this.$refs.uploadInput.files.length === 0) {
|
||||
return
|
||||
}
|
||||
migrationConfig = this.$refs.uploadInput.files[0]
|
||||
}
|
||||
useTitle(() => t('migrate.titleService', {name: migrator.value.name}))
|
||||
|
||||
try {
|
||||
const {message} = await this.migrationService.migrate(migrationConfig)
|
||||
this.message = message
|
||||
const namespaceStore = useNamespaceStore()
|
||||
return namespaceStore.loadNamespaces()
|
||||
} finally {
|
||||
this.isMigrating = false
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
async function initMigration() {
|
||||
if (migrator.value.isFileMigrator) {
|
||||
return
|
||||
}
|
||||
|
||||
authUrl.value = await migrationService.getAuthUrl().then(({url}) => url)
|
||||
|
||||
const TOKEN_HASH_PREFIX = '#token='
|
||||
|
||||
migratorAuthCode.value = location.hash.startsWith(TOKEN_HASH_PREFIX)
|
||||
? location.hash.substring(TOKEN_HASH_PREFIX.length)
|
||||
: props.code as string
|
||||
|
||||
if (!migratorAuthCode.value) {
|
||||
return
|
||||
}
|
||||
const {time} = await migrationService.getStatus()
|
||||
if (time) {
|
||||
lastMigrationDate.value = parseDateOrNull(time)
|
||||
|
||||
if (lastMigrationDate.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
await migrate()
|
||||
}
|
||||
|
||||
initMigration()
|
||||
|
||||
const uploadInput = ref<HTMLInputElement | null>(null)
|
||||
async function migrate() {
|
||||
isMigrating.value = true
|
||||
lastMigrationDate.value = null
|
||||
message.value = ''
|
||||
|
||||
let migrationConfig: MigrationConfig | File = {code: migratorAuthCode.value}
|
||||
|
||||
dpschen
commented
@konrad: @konrad:
This `MigrationConfig` here is wrong. Since the config is passed to the `migrate` method of the service which passes it to update, which wants a `File` (?) I'm not sure what type to define here. Any idea?
konrad
commented
Maybe Maybe `MigrationConfig | File`? The `File` type is correct but only when the migrator requires a file to be uploaded. Otherwise it needs that code to make api calls to the third party api.
dpschen
commented
The update function is defined in the The update function is defined in the `AbstractService` thus it's types as well.
Have to find out how we can pass types there so that it supports different types depending on the use-case. I think it doesn't make sense to support the MigrationConfig type there.
|
||||
if (migrator.value.isFileMigrator) {
|
||||
if (uploadInput.value?.files?.length === 0) {
|
||||
return
|
||||
}
|
||||
migrationConfig = uploadInput.value?.files?.[0] as File
|
||||
}
|
||||
|
||||
try {
|
||||
const result = migrator.value.isFileMigrator
|
||||
? await migrationFileService.migrate(migrationConfig as File)
|
||||
: await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
message.value = result.message
|
||||
const namespaceStore = useNamespaceStore()
|
||||
return namespaceStore.loadNamespaces()
|
||||
} finally {
|
||||
isMigrating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 471 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
@ -49,4 +49,4 @@ export const MIGRATORS: IMigratorRecord = {
|
|||
icon: tickTickIcon as string,
|
||||
isFileMigrator: true,
|
||||
},
|
||||
}
|
||||
} as const
|
It looks like this call to the api happens but the
authUrl
stays empty.