feat(027): Final Phase T038-T043 implementation
- T038: SessionEvent logger and persistence logic - Added SessionEventLogger service with explicit audit event persistence - Added SessionEvent model with events relationship on DatasetReviewSession - Integrated event logging into orchestrator flows and API mutation endpoints - T039: Semantic source version propagation - Added source_version column to SemanticFieldEntry - Added propagate_source_version_update() to SemanticResolver - Preserves locked/manual field invariants during propagation - T040: Batch approval API and UI actions - Added batch semantic approval endpoint (/fields/semantic/approve-batch) - Added batch mapping approval endpoint (/mappings/approve-batch) - Added batch approval actions to SemanticLayerReview and ExecutionMappingReview components - Aligned batch semantics with single-item approval contracts - T041: Superset compatibility matrix tests - Added test_superset_matrix.py with preview and SQL Lab fallback coverage - Tests verify client method preference and matrix fallback behavior - T042: RBAC audit sweep on session-mutation endpoints - Added _require_owner_mutation_scope() helper - Applied owner guards to update_session, delete_session, and all mutation endpoints - Ensured no bypass of existing permission checks - T043: i18n coverage for dataset-review UI - Added workspace state labels (empty/importing/review) to en.json and ru.json - Added batch action labels for semantics and mappings - Fixed workspace state comparison to lowercase strings - Removed hardcoded workspace state display strings Signed-off-by: Implementation Specialist <impl@ss-tools>
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
<!-- [DEF:LaunchConfirmationPanel:Component] -->
|
||||
<!-- @COMPLEXITY: 4 -->
|
||||
<!-- @SEMANTICS: dataset-review, launch-confirmation, run-gates, sql-lab, audited-execution -->
|
||||
<!-- @PURPOSE: Summarize final execution context, surface launch blockers explicitly, and confirm only a gate-complete SQL Lab launch request. -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[api_module] -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[DatasetReviewWorkspace] -->
|
||||
<!-- @PRE: Session detail, mappings, findings, preview state, and latest run context belong to the current ownership-scoped session payload. -->
|
||||
<!-- @POST: Users can see why launch is blocked or confirm a run-ready launch with explicit SQL Lab handoff evidence. -->
|
||||
<!-- @SIDE_EFFECT: Submits the launch request through dataset orchestration APIs and updates the workspace with returned run context state. -->
|
||||
<!-- @UX_STATE: Blocked -> Explicit blocker list prevents hidden bypass of approvals, readiness, or preview-fingerprint gates. -->
|
||||
<!-- @UX_STATE: Ready -> Final reviewed run context is visible before confirmation. -->
|
||||
<!-- @UX_STATE: Submitted -> SQL Lab handoff and audited run context reference are shown after launch request succeeds. -->
|
||||
<!-- @UX_FEEDBACK: Launch button, blocker list, and success state all reflect current gate truth instead of generic confirmation copy. -->
|
||||
<!-- @UX_RECOVERY: Blocked launch state provides jump paths back to mapping review, preview generation, or validation remediation. -->
|
||||
<!-- @UX_REACTIVITY: Uses $props, $state, and $derived only; no legacy reactive syntax. -->
|
||||
<script>
|
||||
import { t } from "$lib/i18n";
|
||||
import { requestApi } from "$lib/api.js";
|
||||
|
||||
let {
|
||||
sessionId = "",
|
||||
session = null,
|
||||
findings = [],
|
||||
mappings = [],
|
||||
preview = null,
|
||||
previewState = "",
|
||||
latestRunContext = null,
|
||||
disabled = false,
|
||||
onupdated = () => {},
|
||||
onjump = () => {},
|
||||
} = $props();
|
||||
|
||||
let launchStatus = $state("");
|
||||
let launchMessage = $state("");
|
||||
let launchResult = $state(null);
|
||||
|
||||
const activeRunContext = $derived(launchResult?.run_context || latestRunContext || null);
|
||||
const effectivePreviewState = $derived(
|
||||
String(previewState || preview?.preview_status || "missing"),
|
||||
);
|
||||
const approvedMappingsCount = $derived(
|
||||
(mappings || []).filter((mapping) => mapping.approval_state === "approved").length,
|
||||
);
|
||||
const launchBlockers = $derived(buildLaunchBlockers());
|
||||
const panelState = $derived(
|
||||
activeRunContext ? "Submitted" : launchBlockers.length > 0 ? "Blocked" : "Ready",
|
||||
);
|
||||
const launchButtonLabel = $derived(
|
||||
launchStatus === "saving"
|
||||
? $t.dataset_review?.launch?.launch_loading
|
||||
: $t.dataset_review?.launch?.launch_action,
|
||||
);
|
||||
|
||||
function buildLaunchBlockers() {
|
||||
const blockers = [];
|
||||
|
||||
for (const finding of findings || []) {
|
||||
if (
|
||||
finding.severity === "blocking" &&
|
||||
finding.resolution_state !== "resolved" &&
|
||||
finding.resolution_state !== "approved"
|
||||
) {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.blocking_finding ||
|
||||
"Blocking findings remain unresolved",
|
||||
detail: finding.title || finding.message || finding.code,
|
||||
target: "summary",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const mapping of mappings || []) {
|
||||
if (mapping.requires_explicit_approval && mapping.approval_state !== "approved") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.mapping_approval_required ||
|
||||
"Mapping approval is still required",
|
||||
detail: mapping.transformation_note || mapping.mapping_id,
|
||||
target: "mapping",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (effectivePreviewState === "missing") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.preview_missing ||
|
||||
"Superset preview is required before launch",
|
||||
detail: "",
|
||||
target: "sql-preview",
|
||||
});
|
||||
} else if (effectivePreviewState === "pending") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.preview_pending ||
|
||||
"Preview generation is still in progress",
|
||||
detail: "",
|
||||
target: "sql-preview",
|
||||
});
|
||||
} else if (effectivePreviewState === "stale") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.preview_stale ||
|
||||
"Preview no longer matches the current execution inputs",
|
||||
detail: "",
|
||||
target: "sql-preview",
|
||||
});
|
||||
} else if (effectivePreviewState === "failed") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.preview_failed ||
|
||||
"Preview failed and launch remains blocked",
|
||||
detail: preview?.error_details || preview?.error_code || "",
|
||||
target: "sql-preview",
|
||||
});
|
||||
}
|
||||
|
||||
if (!activeRunContext && String(session?.readiness_state || "") !== "run_ready") {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.readiness_not_run_ready ||
|
||||
"Session is not yet in run-ready state",
|
||||
detail: session?.readiness_state || "",
|
||||
target: "next-action",
|
||||
});
|
||||
}
|
||||
|
||||
if (!activeRunContext && !preview?.preview_fingerprint) {
|
||||
blockers.push({
|
||||
label:
|
||||
$t.dataset_review?.launch?.blockers?.preview_fingerprint_missing ||
|
||||
"Preview fingerprint is missing, so launch cannot be trusted",
|
||||
detail: "",
|
||||
target: "sql-preview",
|
||||
});
|
||||
}
|
||||
|
||||
return blockers;
|
||||
}
|
||||
|
||||
function getStateLabel(state) {
|
||||
return $t.dataset_review?.launch?.state?.[state] || state;
|
||||
}
|
||||
|
||||
function getStateClass(state) {
|
||||
if (state === "Submitted") {
|
||||
return "bg-emerald-100 text-emerald-700";
|
||||
}
|
||||
if (state === "Ready") {
|
||||
return "bg-blue-100 text-blue-700";
|
||||
}
|
||||
return "bg-red-100 text-red-700";
|
||||
}
|
||||
|
||||
function jumpTo(target) {
|
||||
onjump({ target });
|
||||
}
|
||||
|
||||
async function confirmLaunch() {
|
||||
if (!sessionId || disabled || panelState !== "Ready" || launchStatus === "saving") {
|
||||
return;
|
||||
}
|
||||
|
||||
launchStatus = "saving";
|
||||
launchMessage = "";
|
||||
|
||||
try {
|
||||
const result = await requestApi(`/dataset-orchestration/sessions/${sessionId}/launch`, "POST");
|
||||
launchResult = result;
|
||||
launchStatus = "success";
|
||||
launchMessage = $t.dataset_review?.launch?.messages?.launch_started;
|
||||
onupdated({
|
||||
launch_result: result,
|
||||
preview_state: "unchanged",
|
||||
});
|
||||
} catch (error) {
|
||||
launchStatus = "error";
|
||||
launchMessage =
|
||||
error?.message ||
|
||||
$t.dataset_review?.launch?.messages?.launch_failed ||
|
||||
$t.common?.error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section id="launch" class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.eyebrow}
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-slate-900">
|
||||
{$t.dataset_review?.launch?.title}
|
||||
</h2>
|
||||
<p class="mt-1 max-w-3xl text-sm text-slate-600">
|
||||
{$t.dataset_review?.launch?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span class={`rounded-full px-3 py-1 text-xs font-medium ${getStateClass(panelState)}`}>
|
||||
{$t.dataset_review?.launch?.state_label}: {getStateLabel(panelState)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if panelState === "Blocked"}
|
||||
<div class="mt-5 rounded-xl border border-red-200 bg-red-50 p-4">
|
||||
<h3 class="text-sm font-semibold text-red-800">
|
||||
{$t.dataset_review?.launch?.blocked_title}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-red-700">
|
||||
{$t.dataset_review?.launch?.blocked_body}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each launchBlockers as blocker}
|
||||
<div class="rounded-xl border border-red-200 bg-white p-3">
|
||||
<div class="text-sm font-medium text-slate-900">{blocker.label}</div>
|
||||
{#if blocker.detail}
|
||||
<div class="mt-1 break-all text-xs text-slate-600">{blocker.detail}</div>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex items-center justify-center rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
onclick={() => jumpTo(blocker.target)}
|
||||
>
|
||||
{$t.dataset_review?.launch?.resolve_action}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if panelState === "Ready"}
|
||||
<div class="mt-5 space-y-4">
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.dataset_ref_label}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-slate-900">
|
||||
{session?.dataset_ref || ($t.common?.unknown || "unknown")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.readiness_label}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-slate-900">
|
||||
{session?.readiness_state || ($t.common?.unknown || "unknown")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.approved_mappings_label}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-slate-900">
|
||||
{approvedMappingsCount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.preview_fingerprint_label}
|
||||
</div>
|
||||
<div class="mt-1 break-all text-sm font-medium text-slate-900">
|
||||
{preview?.preview_fingerprint || ($t.common?.not_available || "N/A")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<h3 class="text-sm font-semibold text-slate-900">
|
||||
{$t.dataset_review?.launch?.sql_lab_target_title}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-slate-600">
|
||||
{$t.dataset_review?.launch?.sql_lab_target_body}
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
<span class="rounded-full bg-white px-2 py-1">
|
||||
{$t.dataset_review?.launch?.preview_status_label}: {preview?.preview_status}
|
||||
</span>
|
||||
<span class="rounded-full bg-white px-2 py-1">
|
||||
{$t.dataset_review?.launch?.compiled_by_label}: {preview?.compiled_by}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={confirmLaunch}
|
||||
disabled={disabled || launchStatus === "saving"}
|
||||
class="inline-flex w-full items-center justify-center rounded-xl bg-blue-600 px-4 py-3 text-sm font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-slate-300"
|
||||
>
|
||||
{launchButtonLabel}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if panelState === "Submitted"}
|
||||
<div class="mt-5 space-y-4 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-emerald-900">
|
||||
{$t.dataset_review?.launch?.submitted_title}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-emerald-800">
|
||||
{$t.dataset_review?.launch?.submitted_body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-xl border border-emerald-200 bg-white p-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.run_context_label}
|
||||
</div>
|
||||
<div class="mt-1 break-all text-sm font-medium text-slate-900">
|
||||
{activeRunContext?.run_context_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-emerald-200 bg-white p-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.sql_lab_session_label}
|
||||
</div>
|
||||
<div class="mt-1 break-all text-sm font-medium text-slate-900">
|
||||
{activeRunContext?.sql_lab_session_ref}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-emerald-200 bg-white p-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.launch_status_label}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-slate-900">
|
||||
{activeRunContext?.launch_status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-emerald-200 bg-white p-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.launch?.preview_ref_label}
|
||||
</div>
|
||||
<div class="mt-1 break-all text-sm font-medium text-slate-900">
|
||||
{activeRunContext?.preview_id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if launchMessage}
|
||||
<div
|
||||
class={`mt-4 rounded-xl px-3 py-3 text-sm ${
|
||||
launchStatus === "error"
|
||||
? "border border-red-200 bg-red-50 text-red-700"
|
||||
: launchStatus === "saving"
|
||||
? "border border-blue-200 bg-blue-50 text-blue-700"
|
||||
: "border border-emerald-200 bg-emerald-50 text-emerald-800"
|
||||
}`}
|
||||
>
|
||||
{launchMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- [/DEF:LaunchConfirmationPanel:Component] -->
|
||||
Reference in New Issue
Block a user