mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-23 16:03:33 +00:00
update
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user