This commit is contained in:
dieriba
2025-10-23 23:13:23 +02:00
parent 00e28d4749
commit 64325bc9b1
5 changed files with 737 additions and 119 deletions
+83
View File
@@ -834,6 +834,67 @@ paths:
schema:
$ref: "#/components/schemas/MigrationStatus"
/workspaces/migrate/complete:
post:
summary: complete workspace migration by migrating authentication tables
operationId: completeWorkspaceMigration
tags:
- workspace
requestBody:
description: complete migration request
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CompleteWorkspaceMigrationRequest"
responses:
"200":
description: migration completed
content:
text/plain:
schema:
type: string
/workspaces/migrate/{workspace}/revert:
post:
summary: revert incomplete workspace migration
operationId: revertWorkspaceMigration
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: revert migration request
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RevertWorkspaceMigrationRequest"
responses:
"200":
description: migration reverted
content:
text/plain:
schema:
type: string
/workspaces/migrate/{workspace}/incomplete:
get:
summary: get incomplete migration for source workspace
operationId: getIncompleteMigration
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: target workspace id if incomplete migration exists
content:
application/json:
schema:
type: string
nullable: true
/settings/get_ducklake_instance_catalog_db_status:
post:
summary: Returns the set-up statuses of ducklake instance catalog dbs
@@ -17723,6 +17784,28 @@ components:
- target_workspace_name
- target_workspace_id
CompleteWorkspaceMigrationRequest:
type: object
properties:
source_workspace_id:
type: string
description: source workspace id
target_workspace_id:
type: string
description: target workspace id
required:
- source_workspace_id
- target_workspace_id
RevertWorkspaceMigrationRequest:
type: object
properties:
target_workspace_id:
type: string
description: target workspace id to revert
required:
- target_workspace_id
MigrateJobsRequest:
type: object
properties:
+5 -2
View File
@@ -67,7 +67,7 @@ use crate::teams_oss::{
workspaces_list_available_teams_channels, workspaces_list_available_teams_ids,
};
use crate::workspaces_extra::{get_migration_status, migrate_jobs, migrate_workspace};
use crate::workspaces_extra::{get_migration_status, migrate_jobs, migrate_workspace, complete_workspace_migration, revert_workspace_migration, get_incomplete_migration};
lazy_static::lazy_static! {
static ref WORKSPACE_KEY_REGEXP: Regex = Regex::new("^[a-zA-Z0-9]{64}$").unwrap();
@@ -174,7 +174,10 @@ pub fn migrate_service() -> Router {
Router::new()
.route("/tables", post(migrate_workspace))
.route("/jobs", post(migrate_jobs))
.route("/status", get(get_migration_status)),
.route("/status", get(get_migration_status))
.route("/complete", post(complete_workspace_migration))
.route("/:workspace/revert", post(revert_workspace_migration))
.route("/:workspace/incomplete", get(get_incomplete_migration)),
)
}
pub fn global_service() -> Router {
+219 -13
View File
@@ -1,6 +1,9 @@
use crate::db::ApiAuthed;
use crate::workspaces::{check_w_id_conflict, CREATE_WORKSPACE_REQUIRE_SUPERADMIN, MIGRATE_JOBS_WORKSPACE_REQUIRE_SUPERADMIN, WM_FORK_PREFIX};
use crate::workspaces::{
check_w_id_conflict, CREATE_WORKSPACE_REQUIRE_SUPERADMIN,
MIGRATE_JOBS_WORKSPACE_REQUIRE_SUPERADMIN, WM_FORK_PREFIX,
};
use crate::{db::DB, utils::require_super_admin};
use axum::extract::Query;
@@ -275,16 +278,17 @@ pub async fn migrate_workspace(
sqlx::query!(
r#"
INSERT INTO
workspace
SELECT $1, $2, owner, deleted, premium
workspace (id, name, owner, deleted, premium, parent_workspace_id)
SELECT $1, $2, owner, deleted, premium, $3
FROM
workspace
WHERE
id = $3
id = $4
"#,
&req.target_workspace_id,
&req.target_workspace_name,
&req.source_workspace_id
&req.source_workspace_id,
&req.source_workspace_id,
)
.execute(&mut *tx)
.await?;
@@ -321,7 +325,7 @@ async fn migrate_metadata_tables(
source: &str,
target: &str,
) -> Result<()> {
let simple_tables = [
let non_auth_tables = [
"account",
"app",
"audit",
@@ -346,20 +350,14 @@ async fn migrate_metadata_tables(
"resource_type",
"schedule",
"script",
"token",
"usr",
"variable",
"workspace_env",
"workspace_invite",
"workspace_key",
"workspace_settings",
"job_logs",
"job_stats",
"v2_job_queue",
"v2_job",
];
for table in simple_tables {
for table in non_auth_tables {
sqlx::query(&format!(
"UPDATE {} SET workspace_id = $1 WHERE workspace_id = $2",
table
@@ -416,6 +414,214 @@ async fn migrate_metadata_tables(
Ok(())
}
async fn migrate_auth_tables(
tx: &mut Transaction<'_, Postgres>,
source: &str,
target: &str,
) -> Result<()> {
let auth_tables = [
"token",
"usr",
"workspace_env",
"workspace_invite",
"workspace_key",
"workspace_settings",
];
for table in auth_tables {
sqlx::query(&format!(
"UPDATE {} SET workspace_id = $1 WHERE workspace_id = $2",
table
))
.bind(target)
.bind(source)
.execute(&mut **tx)
.await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct CompleteWorkspaceMigrationRequest {
source_workspace_id: String,
target_workspace_id: String,
}
pub async fn complete_workspace_migration(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Json(req): Json<CompleteWorkspaceMigrationRequest>,
) -> Result<String> {
is_allowed_to_migrate(&db, &authed, *CREATE_WORKSPACE_REQUIRE_SUPERADMIN).await?;
let mut tx = db.begin().await?;
let source_workspace_id = sqlx::query_scalar!(
"SELECT parent_workspace_id FROM workspace WHERE id = $1",
&req.target_workspace_id
)
.fetch_optional(&mut *tx)
.await?
.flatten();
match source_workspace_id {
Some(workspace) => {
if workspace != req.source_workspace_id {
return Err(Error::BadRequest(
"Invalid migration: target workspace was not created from source workspace"
.to_string(),
));
}
}
None => {
return Err(Error::BadRequest(
"Target workspace does not exist".to_string(),
));
}
}
migrate_auth_tables(&mut tx, &req.source_workspace_id, &req.target_workspace_id).await?;
sqlx::query!(
"UPDATE workspace SET parent_workspace_id = NULL WHERE id = $1",
&req.target_workspace_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"workspace.complete_migration",
ActionKind::Update,
&req.target_workspace_id,
Some(&authed.email),
Some(
[
("source", req.source_workspace_id.as_str()),
("target", req.target_workspace_id.as_str()),
]
.into(),
),
)
.await?;
tx.commit().await?;
Ok(format!(
"Completed migration from {} to {}",
&req.source_workspace_id, &req.target_workspace_id
))
}
#[derive(Deserialize)]
pub struct RevertWorkspaceMigrationRequest {
target_workspace_id: String,
}
pub async fn revert_workspace_migration(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(req): Json<RevertWorkspaceMigrationRequest>,
) -> Result<String> {
is_allowed_to_migrate(&db, &authed, *CREATE_WORKSPACE_REQUIRE_SUPERADMIN).await?;
let mut tx = db.begin().await?;
let from_workspace_id = sqlx::query_scalar!(
r#"
SELECT
parent_workspace_id
FROM
workspace
WHERE
id = $1
"#,
&req.target_workspace_id,
)
.fetch_optional(&mut *tx)
.await?
.flatten();
let source_workspace_id = match from_workspace_id {
Some(source_workspace_id) => {
if source_workspace_id != w_id {
return Err(Error::BadRequest(
"No incomplete migration found for this workspace".to_string(),
));
}
source_workspace_id
}
None => {
return Err(Error::BadRequest(
"Target workspace does not exist".to_string(),
));
}
};
migrate_metadata_tables(&mut tx, &req.target_workspace_id, &source_workspace_id).await?;
sqlx::query!(
r#"DELETE FROM workspace WHERE id = $1"#,
req.target_workspace_id
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
"workspace.revert_migration",
ActionKind::Delete,
&req.target_workspace_id,
Some(&authed.email),
Some(
[
("source", source_workspace_id.as_str()),
("target", req.target_workspace_id.as_str()),
]
.into(),
),
)
.await?;
tx.commit().await?;
Ok(format!(
"Reverted migration for workspace {}",
&req.target_workspace_id
))
}
pub async fn get_incomplete_migration(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
) -> JsonResult<Option<String>> {
require_admin(authed.is_admin, &authed.username)?;
let target_workspace = sqlx::query_scalar!(
r#"
SELECT
w.id
FROM
workspace w
WHERE
w.parent_workspace_id = $1
AND NOT EXISTS (
SELECT 1 FROM usr u WHERE u.workspace_id = w.id
)
"#,
w_id
)
.fetch_optional(&db)
.await?;
Ok(Json(target_workspace))
}
const DEFAULT_BATCH_SIZE: i64 = 10000;
#[derive(Serialize)]
@@ -5,15 +5,21 @@
import { sendUserToast } from '$lib/toast'
import { WorkspaceService, JobService } from '$lib/gen'
import Modal from '../common/modal/Modal.svelte'
import { Pen } from 'lucide-svelte'
import { Pen, Loader2 } from 'lucide-svelte'
import { isCloudHosted } from '$lib/cloud'
import { onDestroy } from 'svelte'
import { onDestroy, onMount } from 'svelte'
import { hubPaths } from '$lib/hub'
let { open = $bindable(false) } = $props()
let newName = $state('')
let newId = $derived(newName.toLowerCase().replace(/\s/gi, '-'))
let newId = $state('')
$effect(() => {
if (!incompleteMigration && newName) {
newId = newName.toLowerCase().replace(/\s/gi, '-')
}
})
let checking = $state(false)
let errorId = $state('')
@@ -44,6 +50,44 @@
let jobMigrationComplete = $state(false)
let pollInterval: number | null = null
let incompleteMigration = $state<{
targetWorkspaceId: string
sourceWorkspaceId: string
} | null>(null)
let checkingIncomplete = $state(false)
async function checkForIncompleteMigration() {
if (!$workspaceStore) return
console.log('Checking for incomplete migration for workspace:', $workspaceStore)
try {
checkingIncomplete = true
const targetWorkspaceId = await WorkspaceService.getIncompleteMigration({
workspace: $workspaceStore
})
console.log('Incomplete migration check result:', targetWorkspaceId)
if (targetWorkspaceId) {
incompleteMigration = {
targetWorkspaceId,
sourceWorkspaceId: $workspaceStore
}
oldWorkspaceId = $workspaceStore
newId = targetWorkspaceId
newName = targetWorkspaceId.replace(/-/g, ' ')
metadataMigrated = true
}
} catch (err: any) {
console.error('Error checking for incomplete migration:', err)
} finally {
checkingIncomplete = false
}
}
onMount(() => {
checkForIncompleteMigration()
})
async function migrateMetadata() {
try {
loading = true
@@ -58,7 +102,7 @@
})
metadataMigrated = true
open = false
// Keep modal open to show step 2
sendUserToast(`Workspace metadata migrated to ${newId}`)
} catch (err: any) {
@@ -99,9 +143,10 @@
workspace: $workspaceStore!,
path: hubPaths.workspaceMigrator,
requestBody: {
source_workspace_id: oldWorkspaceId,
target_workspace_id: newId,
target_workspace_name: newName
args: {
source_workspace_id: oldWorkspaceId,
target_workspace_id: newId
}
}
})
@@ -138,15 +183,22 @@
try {
loading = true
await WorkspaceService.completeWorkspaceMigration({
requestBody: {
source_workspace_id: oldWorkspaceId,
target_workspace_id: newId
}
})
await WorkspaceService.deleteWorkspace({
workspace: oldWorkspaceId
})
sendUserToast(`Old workspace ${oldWorkspaceId} deleted`)
sendUserToast(`Migration completed and old workspace deleted`)
window.location.href = `/workspace_settings?tab=general&workspace=${newId}`
} catch (err: any) {
sendUserToast(`Failed to delete workspace: ${err.body || err.message}`, true)
sendUserToast(`Failed to complete migration: ${err.body || err.message}`, true)
} finally {
loading = false
}
@@ -165,6 +217,64 @@
}
}
async function completeMigration() {
if (!incompleteMigration) return
try {
loading = true
await WorkspaceService.completeWorkspaceMigration({
requestBody: {
source_workspace_id: incompleteMigration.sourceWorkspaceId,
target_workspace_id: incompleteMigration.targetWorkspaceId
}
})
sendUserToast('Migration completed successfully!')
window.location.href = `/workspace_settings?tab=general&workspace=${incompleteMigration.targetWorkspaceId}`
} catch (err: any) {
sendUserToast(`Failed to complete migration: ${err.body || err.message}`, true)
} finally {
loading = false
}
}
async function revertMigration() {
// Handle both incomplete migration and post-step-1 revert cases
const sourceWorkspaceId = incompleteMigration?.sourceWorkspaceId || oldWorkspaceId
const targetWorkspaceId = incompleteMigration?.targetWorkspaceId || newId
if (!sourceWorkspaceId || !targetWorkspaceId) return
try {
loading = true
await WorkspaceService.revertWorkspaceMigration({
workspace: sourceWorkspaceId,
requestBody: {
target_workspace_id: targetWorkspaceId
}
})
sendUserToast('Migration reverted successfully!')
// Reset all state and form values
incompleteMigration = null
metadataMigrated = false
jobMigrationStarted = false
jobMigrationComplete = false
newName = ''
newId = ''
oldWorkspaceId = ''
errorId = ''
} catch (err: any) {
sendUserToast(`Failed to revert migration: ${err.body || err.message}`, true)
} finally {
loading = false
}
}
onDestroy(() => {
stopPolling()
})
@@ -192,25 +302,105 @@
<p class="text-xs text-secondary font-normal">Slug to uniquely identify your workspace</p>
</div>
<Modal bind:open title="Change Workspace">
<Modal bind:open title="Change Workspace ID">
<div class="flex flex-col gap-6">
<Alert type="info" title="Two-Step Process">
<div class="text-sm space-y-2">
<p class="font-semibold">This process involves two steps:</p>
<ol class="list-decimal list-inside space-y-1 ml-2">
<li>
<strong>Migrate Metadata</strong> - Migrates workspace configuration, scripts, flows, apps,
resources, schedules, and all metadata
</li>
<li>
<strong>Migrate Jobs</strong>
</li>
</ol>
<p class="text-xs text-secondary mt-2">
After completing step 1, you'll be prompted to proceed with step 2.
</p>
{#if checkingIncomplete}
<Alert type="info" title="Checking for incomplete migrations...">
<p class="text-sm">Please wait while we check for any incomplete workspace migrations.</p>
</Alert>
{/if}
{#if incompleteMigration}
<Alert type="warning" title="Incomplete Migration Detected">
<div class="space-y-3">
<p class="text-sm">
We found an incomplete migration from workspace <strong
>{incompleteMigration.sourceWorkspaceId}</strong
>
to <strong>{incompleteMigration.targetWorkspaceId}</strong>.
</p>
<p class="text-sm"> You can either complete the migration or revert to start over. </p>
</div>
</Alert>
{/if}
{#if metadataMigrated || jobMigrationComplete}
<div class="bg-surface-secondary border rounded-lg p-4">
<h3 class="text-sm font-medium mb-3">Migration Status</h3>
<div class="space-y-2">
<div class="flex items-center gap-3">
<div class="w-4 h-4 rounded-full bg-green-500 flex items-center justify-center">
<span class="text-white text-xs">✓</span>
</div>
<span class="text-sm">Step 1: Metadata migrated</span>
</div>
{#if jobMigrationComplete}
<div class="flex items-center gap-3">
<div class="w-4 h-4 rounded-full bg-green-500 flex items-center justify-center">
<span class="text-white text-xs">✓</span>
</div>
<span class="text-sm">Step 2: Jobs migrated</span>
</div>
{:else}
<div class="flex items-center gap-3">
<div class="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600"></div>
<span class="text-sm text-gray-500">Step 2: Job migration (ready)</span>
</div>
{/if}
</div>
</div>
</Alert>
{/if}
<div
class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-4 rounded-lg"
>
<div class="flex items-center gap-2 mb-2">
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
<span class="text-blue-700 dark:text-blue-300 text-sm font-medium">Current Workspace</span>
</div>
<p class="font-mono text-lg font-bold text-blue-900 dark:text-blue-100"
>{$workspaceStore ?? ''}</p
>
</div>
<div class="space-y-4">
<label class="block">
<span class="text-secondary text-sm font-medium">New Workspace Name</span>
<input
type="text"
bind:value={newName}
placeholder="Enter new workspace name"
class="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed"
disabled={incompleteMigration !== null}
/>
{#if incompleteMigration}
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
Input disabled - migration in progress
</p>
{/if}
</label>
<label class="block">
<span class="text-secondary text-sm font-medium">New Workspace ID</span>
<input
type="text"
bind:value={newId}
placeholder="auto-generated from name"
class="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed"
disabled={incompleteMigration !== null}
/>
{#if errorId}
<div class="text-red-500 text-xs mt-1 flex items-center gap-1">
<span class="w-3 h-3 text-red-500">⚠</span>
{errorId}
</div>
{/if}
{#if incompleteMigration}
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
Input disabled - migration in progress
</p>
{/if}
</label>
</div>
<Alert type="warning" title="Important Notice">
<p class="text-sm">
@@ -218,94 +408,230 @@
accordingly.
</p>
</Alert>
<div class="bg-surface-secondary p-4 rounded-md border border-gray-200 dark:border-gray-700">
<p class="text-secondary text-sm mb-2">Current Workspace ID</p>
<p class="font-bold text-lg">{$workspaceStore ?? ''}</p>
</div>
<label class="block">
<span class="text-secondary text-sm font-medium">New Workspace Name</span>
<input type="text" bind:value={newName} placeholder="Enter new workspace name" class="mt-1" />
</label>
<label class="block">
<span class="text-secondary text-sm font-medium">New Workspace ID</span>
<input type="text" bind:value={newId} placeholder="auto-generated from name" class="mt-1" />
{#if errorId}
<div class="text-red-500 text-xs mt-1">{errorId}</div>
{/if}
</label>
</div>
<svelte:fragment slot="actions">
<Button
size="sm"
disabled={checking || errorId.length > 0 || !newName || !newId}
{loading}
on:click={() => {
migrateMetadata()
}}
>
Step 1: Migrate Metadata
</Button>
{#if incompleteMigration}
<div class="flex gap-3 w-full">
<Button
size="sm"
color="red"
onclick={(e) => {
e.preventDefault()
revertMigration()
}}
{loading}
>
Revert Migration
</Button>
<Button
size="sm"
onclick={(e) => {
e.preventDefault()
completeMigration()
}}
{loading}
>
Complete Migration
</Button>
</div>
{:else if !metadataMigrated}
<div class="flex gap-3">
<Button
size="sm"
disabled={checking || errorId.length > 0 || !newName || !newId || loading}
{loading}
onclick={(e) => {
e.preventDefault()
migrateMetadata()
}}
color="blue"
>
Step 1: Migrate Metadata
</Button>
<Button size="sm" disabled={true} class="opacity-50" color="blue">
Step 2: Migrate Jobs
</Button>
</div>
{:else if metadataMigrated && !jobMigrationComplete}
<div class="flex flex-row gap-2">
<div class="flex gap-3">
<Button size="sm" disabled={true} color="blue">✓ Step 1 Complete</Button>
<Button
size="sm"
disabled={migratingJobs || loading}
onclick={(e) => {
e.preventDefault()
startJobMigration()
}}
color="blue"
>
Step 2: Migrate Jobs
</Button>
</div>
<Button
size="sm"
color="red"
onclick={(e) => {
e.preventDefault()
revertMigration()
}}
{loading}
>
Revert
</Button>
</div>
{:else}
<div class="flex gap-3 w-full">
<Button size="sm" disabled={true} color="blue">✓ Step 1 Complete</Button>
<Button size="sm" disabled={true} color="blue">✓ Step 2 Complete</Button>
</div>
{/if}
</svelte:fragment>
</Modal>
{#if metadataMigrated}
<Alert type="success" title="Step 1 Complete: Metadata Migrated" class="mt-4">
<div class="flex flex-col gap-4">
<div class="space-y-2">
<p class="font-semibold">
✓ Workspace metadata successfully migrated to <strong class="text-primary">{newId}</strong
>
{#if migratingJobs}
<Modal open={true} title="Step 2: Migrating Jobs">
<div class="flex flex-col gap-6">
<div
class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-6 rounded-lg"
>
<div class="flex items-center gap-4 mb-4">
<Loader2 class="w-12 h-12 animate-spin text-blue-500" />
<div>
<h3 class="font-semibold text-blue-900 dark:text-blue-100">Migration in Progress</h3>
<p class="text-sm text-blue-700 dark:text-blue-300"
>This process may take several minutes depending on job history size</p
>
</div>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full animate-pulse" style="width: 60%"></div>
</div>
</div>
<div class="space-y-4">
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span class="text-sm text-secondary">Metadata migration completed</span>
</div>
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
<span class="text-sm font-medium">Migrating job history and results...</span>
</div>
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-gray-300 dark:bg-gray-600 rounded-full"></div>
<span class="text-sm text-gray-400">Authentication tables transfer (pending)</span>
</div>
</div>
<div class="bg-surface-secondary p-4 rounded-lg border">
<p class="text-sm font-medium mb-2">Migration Details</p>
<div class="space-y-1 text-sm text-secondary">
<p><span class="font-medium">From:</span> {oldWorkspaceId}</p>
<p><span class="font-medium">To:</span> {newId}</p>
</div>
</div>
<Alert type="warning" title="Important">
<p class="text-sm">
Please do not close this window or navigate away during the migration process. This
ensures data integrity and allows proper completion tracking.
</p>
</Alert>
</div>
<svelte:fragment slot="actions">
<Button
onclick={(e) => {
e.preventDefault()
cancelJobMigration()
}}
color="red"
size="sm"
class="w-full"
>
Cancel Migration
</Button>
</svelte:fragment>
</Modal>
{/if}
{#if jobMigrationComplete}
<Modal open={true} title="🎉 Migration Complete">
<div class="space-y-6">
<div
class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-6 rounded-lg text-center"
>
<div
class="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4"
>
<span class="text-2xl text-white">✓</span>
</div>
<h3 class="text-lg font-semibold text-green-900 dark:text-green-100 mb-2">
Migration Successful!
</h3>
<p class="text-sm text-green-700 dark:text-green-300">
All data has been successfully migrated to your new workspace
</p>
</div>
{#if !jobMigrationStarted && !jobMigrationComplete}
<Alert type="warning" title="Step 2: Migrate Job History">
<p class="text-sm mb-3">
Job history has not been migrated yet. Click below to start migrating completed jobs
from the old workspace.
</p>
<Button size="sm" onclick={startJobMigration}>Start Job Migration</Button>
</Alert>
{/if}
{#if migratingJobs}
<div
class="bg-surface-secondary p-4 rounded-md border border-gray-200 dark:border-gray-700"
>
<div class="space-y-4">
<div
class="flex items-center gap-2 text-sm text-secondary p-3 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-800"
>
<div
class="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"
></div>
<span class="font-medium">Migrating jobs... This may take a while.</span>
</div>
<Button onclick={cancelJobMigration} color="red" size="sm" class="w-full">
Cancel Migration
</Button>
<div class="bg-surface-secondary p-5 rounded-lg border">
<h4 class="font-medium mb-3">Migration Summary</h4>
<div class="space-y-2 text-sm">
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Workspace metadata migrated</span>
</div>
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Job history and results migrated</span>
</div>
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Ready for final workspace switch</span>
</div>
</div>
{/if}
</div>
{#if jobMigrationComplete}
<Alert type="success" title="Migration Complete">
<div class="space-y-3">
<p class="font-semibold"> ✓ All jobs have been successfully migrated! </p>
<p class="text-sm text-secondary">
Your workspace migration is complete. You can now delete the old workspace and switch
to the new one.
</p>
<Button size="sm" {loading} onclick={deleteOldWorkspaceAndSwitch}>
Delete Old Workspace & Switch
</Button>
</div>
</Alert>
{/if}
<div class="grid grid-cols-2 gap-4">
<div
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 rounded-lg"
>
<p class="text-xs text-red-600 dark:text-red-400 font-medium mb-1">Old Workspace</p>
<p class="font-mono text-sm text-red-800 dark:text-red-200">{oldWorkspaceId}</p>
<p class="text-xs text-red-600 dark:text-red-400 mt-1">Will be deleted</p>
</div>
<div
class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4 rounded-lg"
>
<p class="text-xs text-green-600 dark:text-green-400 font-medium mb-1">New Workspace</p>
<p class="font-mono text-sm text-green-800 dark:text-green-200">{newId}</p>
<p class="text-xs text-green-600 dark:text-green-400 mt-1">Ready to use</p>
</div>
</div>
<Alert type="info" title="Final Step">
<p class="text-sm">
Click the button below to complete the migration process. This will transfer
authentication tables, delete the old workspace, and redirect you to the new workspace.
</p>
</Alert>
</div>
</Alert>
<svelte:fragment slot="actions">
<Button
size="sm"
{loading}
onclick={(e) => {
e.preventDefault()
deleteOldWorkspaceAndSwitch()
}}
class="w-full"
>
<span class="flex items-center justify-center gap-2">
<span>🚀</span>
Complete Migration & Switch to New Workspace
</span>
</Button>
</svelte:fragment>
</Modal>
{/if}
+1 -1
View File
@@ -39,5 +39,5 @@
"smtpReport": "hub/9086/smtp",
"cloneRepoToS3forGitRepoViewer_0": "hub/19825/clone_repo_and_upload_to_instance_storage",
"cloneRepoToS3forGitRepoViewer": "hub/19827/clone_repo_and_upload_to_instance_storage",
"workspaceMigrator": "hub/28053/workspace%20job%20migrator"
"workspaceMigrator": "hub/28054/workspace%20job%20migrator"
}