Wire tenant archive export in frontend
This commit is contained in:
@@ -55,6 +55,22 @@ export type TenantImportResult = {
|
|||||||
files: { restored: number; skipped: number }
|
files: { restored: number; skipped: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TenantExportJob = {
|
||||||
|
exportId: string
|
||||||
|
tenantId?: number
|
||||||
|
status: "queued" | "running" | "ready" | "failed" | string
|
||||||
|
filename: string
|
||||||
|
fileSize?: number | null
|
||||||
|
filesDone?: number
|
||||||
|
filesTotal?: number
|
||||||
|
error?: string | null
|
||||||
|
statusUrl?: string
|
||||||
|
downloadUrl?: string | null
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
completedAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type SystemStatus = {
|
export type SystemStatus = {
|
||||||
checkedAt: string
|
checkedAt: string
|
||||||
backend: {
|
backend: {
|
||||||
@@ -173,13 +189,23 @@ export const useAdmin = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportTenant = async (id: number): Promise<Blob> => {
|
const startTenantExport = async (id: number): Promise<TenantExportJob> => {
|
||||||
return await $api(`/api/admin/tenants/${id}/export`, {
|
return await $api(`/api/admin/tenants/${id}/exports`, {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTenantExport = async (exportId: string): Promise<TenantExportJob> => {
|
||||||
|
return await $api(`/api/admin/tenant-exports/${exportId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadTenantExport = async (exportId: string): Promise<Blob> => {
|
||||||
|
return await $api(`/api/admin/tenant-exports/${exportId}/download`, {
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const importTenant = async (body: Record<string, any>): Promise<TenantImportResult> => {
|
const importTenant = async (body: Record<string, any> | FormData): Promise<TenantImportResult> => {
|
||||||
return await $api("/api/admin/tenant-imports", {
|
return await $api("/api/admin/tenant-imports", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body,
|
body,
|
||||||
@@ -222,7 +248,9 @@ export const useAdmin = () => {
|
|||||||
createTenant,
|
createTenant,
|
||||||
invitePortalUser,
|
invitePortalUser,
|
||||||
updateTenant,
|
updateTenant,
|
||||||
exportTenant,
|
startTenantExport,
|
||||||
|
getTenantExport,
|
||||||
|
downloadTenantExport,
|
||||||
importTenant,
|
importTenant,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ const loading = ref(true)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const exportingTenant = ref(false)
|
const exportingTenant = ref(false)
|
||||||
const importingTenant = ref(false)
|
const importingTenant = ref(false)
|
||||||
|
const tenantExportProgress = ref<null | {
|
||||||
|
status: string
|
||||||
|
filesDone: number
|
||||||
|
filesTotal: number
|
||||||
|
filename: string
|
||||||
|
}>(null)
|
||||||
const creatingUser = ref(false)
|
const creatingUser = ref(false)
|
||||||
const createUserModalOpen = ref(false)
|
const createUserModalOpen = ref(false)
|
||||||
const createdUserPassword = ref("")
|
const createdUserPassword = ref("")
|
||||||
@@ -93,26 +99,46 @@ const downloadTenantExport = async () => {
|
|||||||
if (!tenantForm.value || exportingTenant.value) return
|
if (!tenantForm.value || exportingTenant.value) return
|
||||||
|
|
||||||
exportingTenant.value = true
|
exportingTenant.value = true
|
||||||
|
tenantExportProgress.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await admin.exportTenant(tenantForm.value.id)
|
const started = await admin.startTenantExport(tenantForm.value.id)
|
||||||
|
let job = started
|
||||||
|
|
||||||
|
tenantExportProgress.value = {
|
||||||
|
status: job.status,
|
||||||
|
filesDone: job.filesDone || 0,
|
||||||
|
filesTotal: job.filesTotal || 0,
|
||||||
|
filename: job.filename,
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!["ready", "failed"].includes(job.status)) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||||
|
job = await admin.getTenantExport(started.exportId)
|
||||||
|
tenantExportProgress.value = {
|
||||||
|
status: job.status,
|
||||||
|
filesDone: job.filesDone || 0,
|
||||||
|
filesTotal: job.filesTotal || 0,
|
||||||
|
filename: job.filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.status === "failed") {
|
||||||
|
throw new Error(job.error || "Export konnte nicht erstellt werden.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await admin.downloadTenantExport(started.exportId)
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
const link = document.createElement("a")
|
const link = document.createElement("a")
|
||||||
const safeName = (tenantForm.value.short || tenantForm.value.name || tenantForm.value.id)
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.replace(/[^a-z0-9_-]+/gi, "-")
|
|
||||||
.replace(/^-+|-+$/g, "")
|
|
||||||
.toLowerCase()
|
|
||||||
|
|
||||||
link.href = objectUrl
|
link.href = objectUrl
|
||||||
link.download = `fedeo-tenant-${safeName || tenantForm.value.id}-${new Date().toISOString().slice(0, 10)}.json`
|
link.download = job.filename || `fedeo-tenant-${tenantForm.value.id}.fedeo-export.zip`
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
link.remove()
|
link.remove()
|
||||||
URL.revokeObjectURL(objectUrl)
|
URL.revokeObjectURL(objectUrl)
|
||||||
|
|
||||||
toast.add({ title: "Mandantenexport erstellt", color: "green" })
|
toast.add({ title: "Mandantenexport erstellt", description: job.filename, color: "green" })
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("[administration/tenants/export]", err)
|
console.error("[administration/tenants/export]", err)
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -138,12 +164,22 @@ const importTenantExport = async (event: Event) => {
|
|||||||
lastImportResult.value = null
|
lastImportResult.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawContent = await file.text()
|
const targetTenantId = tenantForm.value?.id || tenantId
|
||||||
const exportData = JSON.parse(rawContent)
|
const isZipExport = file.name.endsWith(".zip") || file.name.endsWith(".fedeo-export.zip") || file.type === "application/zip"
|
||||||
const result = await admin.importTenant({
|
let result
|
||||||
exportData,
|
|
||||||
targetTenantId: tenantForm.value?.id || tenantId,
|
if (isZipExport) {
|
||||||
})
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
formData.append("targetTenantId", String(targetTenantId))
|
||||||
|
result = await admin.importTenant(formData)
|
||||||
|
} else {
|
||||||
|
result = await admin.importTenant({
|
||||||
|
exportData: JSON.parse(await file.text()),
|
||||||
|
targetTenantId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
||||||
|
|
||||||
lastImportResult.value = {
|
lastImportResult.value = {
|
||||||
@@ -290,8 +326,19 @@ onMounted(async () => {
|
|||||||
<div class="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
|
<div class="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
|
||||||
<div class="font-medium">Full Export</div>
|
<div class="font-medium">Full Export</div>
|
||||||
<p class="text-sm text-gray-500 mt-1">
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
Erstellt eine portable JSON-Datei mit Mandantendaten, Benutzerzuordnungen, Rollen, Profilen und Dateiinhalten.
|
Erstellt ein portables ZIP-Archiv mit Mandantendaten, Benutzerzuordnungen, Rollen, Profilen und Dateien.
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="tenantExportProgress" class="mt-4 space-y-2">
|
||||||
|
<UProgress
|
||||||
|
:model-value="tenantExportProgress.filesTotal ? Math.round((tenantExportProgress.filesDone / tenantExportProgress.filesTotal) * 100) : undefined"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ tenantExportProgress.status === 'ready' ? 'Download wird vorbereitet' : 'Export wird erstellt' }}
|
||||||
|
<span v-if="tenantExportProgress.filesTotal">
|
||||||
|
· {{ tenantExportProgress.filesDone }} / {{ tenantExportProgress.filesTotal }} Dateien
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
icon="i-heroicons-arrow-down-tray"
|
icon="i-heroicons-arrow-down-tray"
|
||||||
@@ -310,7 +357,7 @@ onMounted(async () => {
|
|||||||
<input
|
<input
|
||||||
ref="importFileInput"
|
ref="importFileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/json,.json"
|
accept="application/json,application/zip,.json,.zip,.fedeo-export.zip"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="importTenantExport"
|
@change="importTenantExport"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -103,8 +103,17 @@ const importTenantExport = async (event: Event) => {
|
|||||||
importingTenant.value = true
|
importingTenant.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exportData = JSON.parse(await file.text())
|
const isZipExport = file.name.endsWith(".zip") || file.name.endsWith(".fedeo-export.zip") || file.type === "application/zip"
|
||||||
const result = await admin.importTenant(exportData)
|
let result
|
||||||
|
|
||||||
|
if (isZipExport) {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
result = await admin.importTenant(formData)
|
||||||
|
} else {
|
||||||
|
result = await admin.importTenant(JSON.parse(await file.text()))
|
||||||
|
}
|
||||||
|
|
||||||
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
||||||
|
|
||||||
await fetchTenants()
|
await fetchTenants()
|
||||||
@@ -158,7 +167,7 @@ onMounted(async () => {
|
|||||||
<input
|
<input
|
||||||
ref="importFileInput"
|
ref="importFileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/json,.json"
|
accept="application/json,application/zip,.json,.zip,.fedeo-export.zip"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="importTenantExport"
|
@change="importTenantExport"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user