Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions backend/routers/auto_optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ class TriggerRequest(BaseModel):
apply_mode: str = "genie_config"
levers: list[int] | None = None
deploy_target: str | None = None
deploy_space_id: str | None = None
catalog_map: dict[str, str] | None = None


class SchemaAccessStatus(BaseModel):
Expand Down Expand Up @@ -857,6 +859,8 @@ async def trigger(body: TriggerRequest, request: Request):
apply_mode=body.apply_mode,
levers=body.levers,
deploy_target=body.deploy_target,
deploy_space_id=body.deploy_space_id,
catalog_map=body.catalog_map,
)
return {
"runId": result.run_id,
Expand All @@ -879,6 +883,101 @@ async def trigger(body: TriggerRequest, request: Request):
raise HTTPException(status_code=500, detail="Failed to start optimization job.")


class DeployRequest(BaseModel):
target_workspace_url: str
target_space_id: str | None = None
catalog_map: dict[str, str] | None = None


@router.post("/spaces/{space_id}/deploy")
async def deploy_space(space_id: str, body: DeployRequest):
"""Deploy a Genie Space config to a target workspace.

Fetches the current space config, applies catalog remapping, and
PATCHes it to the target workspace. No UC model or optimization run
required — works with any Genie Space.
"""
import json as _json

# 1. Fetch source space config
try:
from backend.services.genie_client import get_serialized_space
space_config = get_serialized_space(genie_space_id=space_id)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to fetch space config: {e}")

# 2. Apply catalog remapping
if body.catalog_map:
remapped = 0
for key in ("tables", "metric_views"):
for src in space_config.get("data_sources", {}).get(key, []):
ident = src.get("identifier", "")
parts = ident.replace("`", "").split(".")
if len(parts) >= 3 and parts[0] in body.catalog_map:
parts[0] = body.catalog_map[parts[0]]
src["identifier"] = ".".join(parts)
remapped += 1
logger.info("Remapped %d catalog references for deploy", remapped)

# 3. Connect to target workspace using the app's SP credentials.
# The SP must be registered in the target workspace.
try:
from databricks.sdk import WorkspaceClient
sp_ws = get_service_principal_client()
target_ws = WorkspaceClient(
host=body.target_workspace_url.rstrip("/"),
client_id=sp_ws.config.client_id,
client_secret=sp_ws.config.client_secret,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to connect to target workspace: {e}")

# 4. Deploy: PATCH existing space or CREATE new one
try:
if body.target_space_id:
# Update existing space
from genie_space_optimizer.common.genie_client import patch_space_config
patch_space_config(target_ws, body.target_space_id, space_config)
target_space = body.target_space_id
logger.info("PATCHed existing space %s in target workspace", target_space)
else:
# Create new space in target workspace
from backend.services.genie_client import get_genie_space
source_space = get_genie_space(genie_space_id=space_id)
display_name = source_space.get("title", "Deployed Genie Space")

# Find a warehouse in the target workspace
warehouses = list(target_ws.warehouses.list())
if not warehouses:
raise ValueError("No SQL warehouse found in target workspace")
target_warehouse_id = warehouses[0].id

response = target_ws.api_client.do(
method="POST",
path="/api/2.0/genie/spaces",
body={
"title": display_name,
"description": f"Deployed from source workspace (space {space_id})",
"parent_path": "/Shared/",
"warehouse_id": target_warehouse_id,
"serialized_space": _json.dumps(space_config),
},
)
target_space = response.get("space_id", "")
logger.info("Created new space %s in target workspace", target_space)

target_host = body.target_workspace_url.rstrip("/")
return {
"status": "DEPLOYED",
"targetSpaceId": target_space,
"targetUrl": target_host,
"spaceUrl": f"{target_host}/genie/rooms/{target_space}",
}
except Exception as e:
logger.exception("Failed to deploy to target workspace: %s", e)
raise HTTPException(status_code=500, detail=f"Deployment failed: {e}")


# ---------------------------------------------------------------------------
# Pipeline step definitions — group raw sub-stages into 6 logical steps
# (Ported from Genie Space Optimizer's map_stages_to_steps)
Expand Down Expand Up @@ -1162,6 +1261,7 @@ async def get_run(run_id: RunId):
"levers": levers,
"links": links,
"convergenceReason": run.get("convergence_reason"),
"deployTarget": run.get("deploy_target") or None,
"deploymentStatus": run.get("deploy_status"),
"labelingSessionUrl": run.get("labeling_session_url") or None,
"labelingSessionName": run.get("labeling_session_name") or None,
Expand Down
41 changes: 40 additions & 1 deletion frontend/src/components/auto-optimize/RunDetailView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"
import { ArrowLeft, Cog, UserCheck, ExternalLink } from "lucide-react"
import { ArrowLeft, Cog, UserCheck, ExternalLink, CheckCircle2, Clock, AlertTriangle } from "lucide-react"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { ScoreSummary } from "@/components/auto-optimize/ScoreSummary"
Expand Down Expand Up @@ -157,6 +157,45 @@ export function RunDetailView({ runId, onBack }: RunDetailViewProps) {
</div>
)}

{/* Deployment Status Banner (when already deployed) */}
{run.deployTarget && run.deploymentStatus && (
<div className={`flex items-start gap-3 rounded-lg border px-4 py-3 ${
run.deploymentStatus === "DEPLOYED"
? "border-emerald-500/30 bg-emerald-500/5"
: run.deploymentStatus === "FAILED"
? "border-red-500/30 bg-red-500/5"
: "border-blue-500/30 bg-blue-500/5"
}`}>
{run.deploymentStatus === "DEPLOYED" ? (
<CheckCircle2 className="mt-0.5 h-5 w-5 text-emerald-600 shrink-0" />
) : run.deploymentStatus === "FAILED" ? (
<AlertTriangle className="mt-0.5 h-5 w-5 text-red-600 shrink-0" />
) : (
<Clock className="mt-0.5 h-5 w-5 text-blue-600 shrink-0" />
)}
<div className="flex-1">
<h3 className={`text-sm font-semibold ${
run.deploymentStatus === "DEPLOYED" ? "text-emerald-900 dark:text-emerald-300"
: run.deploymentStatus === "FAILED" ? "text-red-900 dark:text-red-300"
: "text-blue-900 dark:text-blue-300"
}`}>
{run.deploymentStatus === "DEPLOYED" ? "Deployed to target workspace"
: run.deploymentStatus === "PENDING_APPROVAL" ? "Deployment awaiting approval"
: run.deploymentStatus === "FAILED" ? "Deployment failed"
: `Deployment: ${run.deploymentStatus}`}
</h3>
<p className="mt-0.5 text-xs text-muted">Target: {run.deployTarget}</p>
</div>
{run.deploymentStatus === "DEPLOYED" && (
<a href={run.deployTarget} target="_blank" rel="noopener noreferrer"
className="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md border border-emerald-500/30 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-500/10 transition-colors">
Open Workspace <ExternalLink className="h-3.5 w-3.5" />
</a>
)}
</div>
)}


{/* Tabs */}
<div className="flex gap-1 border-b border-default">
<button
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,21 @@ export async function triggerAutoOptimize(request: GSOTriggerRequest): Promise<G
)
}

export async function deploySpace(
spaceId: string,
config: { target_workspace_url: string; target_space_id?: string; catalog_map?: Record<string, string> }
): Promise<{ status: string; targetSpaceId: string; targetUrl: string; spaceUrl?: string }> {
return fetchWithTimeout<{ status: string; targetSpaceId: string; targetUrl: string; spaceUrl?: string }>(
`${API_BASE}/auto-optimize/spaces/${spaceId}/deploy`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
},
LONG_TIMEOUT
)
}

export async function getAutoOptimizeRun(runId: string): Promise<GSOPipelineRun> {
return fetchWithTimeout<GSOPipelineRun>(`${API_BASE}/auto-optimize/runs/${runId}`)
}
Expand Down
Loading