feat: MigrateService script setup #2432

Merged
konrad merged 1 commits from dpschen/frontend:feature/feat-MigrationService-script-setup into main 2022-11-03 14:19:44 +00:00
12 changed files with 105 additions and 111 deletions

View File

@ -31,8 +31,8 @@ import ListTeamsComponent from '../views/teams/ListTeams.vue'
import ListLabelsComponent from '../views/labels/ListLabels.vue' import ListLabelsComponent from '../views/labels/ListLabels.vue'
import NewLabelComponent from '../views/labels/NewLabel.vue' import NewLabelComponent from '../views/labels/NewLabel.vue'
// Migration // Migration
import MigrationComponent from '../views/migrator/Migrate.vue' const MigrationComponent = () => import('@/views/migrate/Migration.vue')
import MigrateServiceComponent from '../views/migrator/MigrateService.vue' const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
// List Views // List Views
import ListList from '../views/list/ListList.vue' import ListList from '../views/list/ListList.vue'
const ListGantt = () => import('../views/list/ListGantt.vue') const ListGantt = () => import('../views/list/ListGantt.vue')
@ -445,7 +445,11 @@ const router = createRouter({
{ {
path: '/migrate/:service', path: '/migrate/:service',
name: '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', path: '/filters/new',

View File

@ -1,11 +1,13 @@
import AbstractService from '../abstractService' 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. // 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. // 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 = '' serviceUrlKey = ''
constructor(serviceUrlKey) { constructor(serviceUrlKey: string) {
super({ super({
update: '/migration/' + serviceUrlKey + '/migrate', update: '/migration/' + serviceUrlKey + '/migrate',
}) })
@ -20,7 +22,7 @@ export default class AbstractMigrationService extends AbstractService {
return this.getM('/migration/' + this.serviceUrlKey + '/status') return this.getM('/migration/' + this.serviceUrlKey + '/status')
} }
migrate(data) { migrate(data: MigrationConfig) {
return this.update(data) return this.update(data)
} }
} }

View File

@ -1,4 +1,3 @@
import type {IFile} from '@/modelTypes/IFile'
import AbstractService from '../abstractService' import AbstractService from '../abstractService'
// This service builds on top of the abstract service and basically just hides away method names. // 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 return false
} }
migrate(file: IFile) { migrate(file: File) {
return this.uploadFile( return this.uploadFile(
this.paths.create, this.paths.create,
file, file,

View File

@ -14,9 +14,9 @@
type="file" type="file"
/> />
<x-button <x-button
:loading="migrationService.loading" :loading="migrationFileService.loading"
:disabled="migrationService.loading || undefined" :disabled="migrationFileService.loading || undefined"
@click="$refs.uploadInput.click()" @click="uploadInput?.click()"
> >
{{ $t('migrate.upload') }} {{ $t('migrate.upload') }}
</x-button> </x-button>
@ -57,129 +57,118 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<message class="mb-4"> <Message class="mb-4">
{{ message }} {{ message }}
</message> </Message>
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button> <x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {
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]
},
},
beforeRouteEnter(to) { beforeRouteEnter(to) {
if (MIGRATORS[to.params.service] === undefined) { if (MIGRATORS[to.params.service as string] === undefined) {
return {name: 'not-found'} return {name: 'not-found'}
} }
}, },
}
</script>
created() { <script setup lang="ts">
this.initMigration() import {computed, ref, shallowReactive} from 'vue'
}, import {useI18n} from 'vue-i18n'
mounted() { import Logo from '@/assets/logo.svg?component'
setTitle(this.$t('migrate.titleService', {name: this.migrator.name})) import Message from '@/components/misc/message.vue'
},
methods: { import AbstractMigrationService, { type MigrationConfig } from '@/services/migrator/abstractMigration'
formatDateLong, import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
async initMigration() { import {formatDateLong} from '@/helpers/time/formatDate'
this.migrationService = this.migrator.isFileMigrator import {parseDateOrNull} from '@/helpers/parseDateOrNull'
? new AbstractMigrationFileService(this.migrator.id)
: new AbstractMigrationService(this.migrator.id)
if (this.migrator.isFileMigrator) { import {MIGRATORS} from './migrators'
return 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=') const props = defineProps<{
? location.hash.substring(7) service: string,
: this.$route.query.code code?: string,
}>()
if (!this.migratorAuthCode) { const {t} = useI18n({useScope: 'global'})
return
}
const {time} = await this.migrationService.getStatus()
if (time) {
this.lastMigrationDate = typeof time === 'string' && time?.startsWith('0001-')
? null
: new Date(time)
if (this.lastMigrationDate) { const progressDotsCount = ref(PROGRESS_DOTS_COUNT)
return const authUrl = ref('')
} const isMigrating = ref(false)
} const lastMigrationDate = ref<Date | null>(null)
await this.migrate() const message = ref('')
}, const migratorAuthCode = ref('')
async migrate() { const migrator = computed(() => MIGRATORS[props.service])
this.isMigrating = true
this.lastMigrationDate = null
this.message = ''
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) { useTitle(() => t('migrate.titleService', {name: migrator.value.name}))
if (this.$refs.uploadInput.files.length === 0) {
return
}
migrationConfig = this.$refs.uploadInput.files[0]
}
try { async function initMigration() {
const {message} = await this.migrationService.migrate(migrationConfig) if (migrator.value.isFileMigrator) {
this.message = message return
const namespaceStore = useNamespaceStore() }
return namespaceStore.loadNamespaces()
} finally { authUrl.value = await migrationService.getAuthUrl().then(({url}) => url)
this.isMigrating = false
} const TOKEN_HASH_PREFIX = '#token='

It looks like this call to the api happens but the authUrl stays empty.

It looks like this call to the api happens but the `authUrl` stays empty.
}, 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}

@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: 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?

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.

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.

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.

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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

View File

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -49,4 +49,4 @@ export const MIGRATORS: IMigratorRecord = {
icon: tickTickIcon as string, icon: tickTickIcon as string,
isFileMigrator: true, isFileMigrator: true,
}, },
} } as const