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,247 @@
|
||||
<!-- [DEF:CompiledSQLPreview:Component] -->
|
||||
<!-- @COMPLEXITY: 4 -->
|
||||
<!-- @SEMANTICS: dataset-review, compiled-sql-preview, superset-preview, stale-state, diagnostics -->
|
||||
<!-- @PURPOSE: Present the exact Superset-generated compiled SQL preview, expose readiness or staleness clearly, and preserve readable recovery paths when preview generation fails. -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[api_module] -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[DatasetReviewWorkspace] -->
|
||||
<!-- @PRE: Session id is available and preview state comes from the current ownership-scoped session detail payload. -->
|
||||
<!-- @POST: Users can distinguish missing, pending, ready, stale, and error preview states and can trigger only Superset-backed preview generation. -->
|
||||
<!-- @SIDE_EFFECT: Requests preview generation through dataset orchestration APIs and updates route shell preview state when Superset responds. -->
|
||||
<!-- @UX_STATE: Missing -> Prompt the user to generate a Superset preview before launch. -->
|
||||
<!-- @UX_STATE: Pending -> Show generation-in-progress feedback without fabricating SQL. -->
|
||||
<!-- @UX_STATE: Ready -> Render read-only SQL preview that is explicitly labeled as compiled by Superset. -->
|
||||
<!-- @UX_STATE: Stale -> Mark the existing preview as outdated after mapping/input changes until regenerated. -->
|
||||
<!-- @UX_STATE: Error -> Show readable Superset compilation diagnostics and preserve remediation action. -->
|
||||
<!-- @UX_FEEDBACK: Preview refresh updates status pill, timestamps, and inline generation feedback. -->
|
||||
<!-- @UX_RECOVERY: Users can retry preview generation and jump back to mapping review when diagnostics point to execution-input issues. -->
|
||||
<!-- @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 = "",
|
||||
preview = null,
|
||||
previewState = "",
|
||||
disabled = false,
|
||||
onupdated = () => {},
|
||||
onjump = () => {},
|
||||
} = $props();
|
||||
|
||||
let localStatus = $state("");
|
||||
let localMessage = $state("");
|
||||
|
||||
const effectiveState = $derived(resolvePreviewState());
|
||||
const compiledBySuperset = $derived(String(preview?.compiled_by || "") === "superset");
|
||||
const hasSql = $derived(Boolean(String(preview?.compiled_sql || "").trim()));
|
||||
const previewTimestamp = $derived(preview?.compiled_at || preview?.created_at || "");
|
||||
|
||||
function resolvePreviewState() {
|
||||
const explicit = String(previewState || "").trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const status = String(preview?.preview_status || "").trim();
|
||||
if (status === "ready" || status === "pending" || status === "failed" || status === "stale") {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (preview?.error_code || preview?.error_details) {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
return "missing";
|
||||
}
|
||||
|
||||
function getStateLabel(state) {
|
||||
return $t.dataset_review?.preview?.state?.[state] || state;
|
||||
}
|
||||
|
||||
function getStateTone(state) {
|
||||
if (state === "ready") {
|
||||
return "bg-emerald-100 text-emerald-700";
|
||||
}
|
||||
if (state === "stale") {
|
||||
return "bg-amber-100 text-amber-700";
|
||||
}
|
||||
if (state === "failed") {
|
||||
return "bg-red-100 text-red-700";
|
||||
}
|
||||
if (state === "pending") {
|
||||
return "bg-blue-100 text-blue-700";
|
||||
}
|
||||
return "bg-slate-100 text-slate-700";
|
||||
}
|
||||
|
||||
function getCompilerLabel() {
|
||||
if (compiledBySuperset) {
|
||||
return $t.dataset_review?.preview?.compiled_by_superset;
|
||||
}
|
||||
return $t.dataset_review?.preview?.compiled_source_unknown;
|
||||
}
|
||||
|
||||
function getPreviewBodyText() {
|
||||
if (effectiveState === "pending") {
|
||||
return $t.dataset_review?.preview?.pending_body;
|
||||
}
|
||||
if (effectiveState === "stale") {
|
||||
return $t.dataset_review?.preview?.stale_body;
|
||||
}
|
||||
if (effectiveState === "failed") {
|
||||
return (
|
||||
preview?.error_details ||
|
||||
preview?.error_code ||
|
||||
$t.dataset_review?.preview?.error_body
|
||||
);
|
||||
}
|
||||
if (effectiveState === "missing") {
|
||||
return $t.dataset_review?.preview?.missing_body;
|
||||
}
|
||||
return $t.dataset_review?.preview?.ready_body;
|
||||
}
|
||||
|
||||
async function requestPreview() {
|
||||
if (!sessionId || disabled || localStatus === "saving") {
|
||||
return;
|
||||
}
|
||||
|
||||
localStatus = "saving";
|
||||
localMessage = "";
|
||||
|
||||
try {
|
||||
const response = await requestApi(
|
||||
`/dataset-orchestration/sessions/${sessionId}/preview`,
|
||||
"POST",
|
||||
);
|
||||
const nextPreview = response?.preview || response;
|
||||
onupdated({
|
||||
preview: nextPreview,
|
||||
preview_state: nextPreview?.preview_status || response?.preview_status || "pending",
|
||||
});
|
||||
localStatus = "success";
|
||||
localMessage = $t.dataset_review?.preview?.messages?.generated;
|
||||
} catch (error) {
|
||||
localStatus = "error";
|
||||
localMessage =
|
||||
error?.message ||
|
||||
$t.dataset_review?.preview?.messages?.generate_failed ||
|
||||
$t.common?.error;
|
||||
}
|
||||
}
|
||||
|
||||
function jumpToMapping() {
|
||||
onjump({ target: "mapping" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<section id="sql-preview" 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?.preview?.eyebrow}
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-slate-900">
|
||||
{$t.dataset_review?.preview?.title}
|
||||
</h2>
|
||||
<p class="mt-1 max-w-3xl text-sm text-slate-600">
|
||||
{$t.dataset_review?.preview?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class={`rounded-full px-3 py-1 text-xs font-medium ${getStateTone(effectiveState)}`}>
|
||||
{$t.dataset_review?.preview?.state_label}: {getStateLabel(effectiveState)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-xl bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-slate-300"
|
||||
onclick={requestPreview}
|
||||
disabled={disabled || localStatus === "saving"}
|
||||
>
|
||||
{localStatus === "saving"
|
||||
? $t.dataset_review?.preview?.generate_loading
|
||||
: $t.dataset_review?.preview?.generate_action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`mt-5 rounded-xl border px-4 py-4 text-sm ${
|
||||
effectiveState === "failed"
|
||||
? "border-red-200 bg-red-50 text-red-700"
|
||||
: effectiveState === "stale"
|
||||
? "border-amber-200 bg-amber-50 text-amber-800"
|
||||
: effectiveState === "pending"
|
||||
? "border-blue-200 bg-blue-50 text-blue-700"
|
||||
: "border-slate-200 bg-slate-50 text-slate-700"
|
||||
}`}
|
||||
>
|
||||
{getPreviewBodyText()}
|
||||
</div>
|
||||
|
||||
{#if preview}
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1">
|
||||
{$t.dataset_review?.preview?.compiler_label}: {getCompilerLabel()}
|
||||
</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1">
|
||||
{$t.dataset_review?.preview?.fingerprint_label}: {preview.preview_fingerprint}
|
||||
</span>
|
||||
{#if previewTimestamp}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1">
|
||||
{$t.dataset_review?.preview?.compiled_at_label}: {previewTimestamp}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if effectiveState === "ready" && hasSql}
|
||||
<div class="mt-5 overflow-hidden rounded-2xl border border-slate-200 bg-slate-950">
|
||||
<div class="flex items-center justify-between border-b border-slate-800 px-4 py-3">
|
||||
<div class="text-sm font-medium text-slate-100">
|
||||
{$t.dataset_review?.preview?.sql_block_title}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
{$t.dataset_review?.preview?.compiled_truth_note}
|
||||
</div>
|
||||
</div>
|
||||
<pre class="overflow-x-auto px-4 py-4 text-sm leading-6 text-slate-100"><code>{preview.compiled_sql}</code></pre>
|
||||
</div>
|
||||
{:else if effectiveState === "failed"}
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
onclick={jumpToMapping}
|
||||
>
|
||||
{$t.dataset_review?.preview?.go_to_mapping_action}
|
||||
</button>
|
||||
</div>
|
||||
{:else if effectiveState === "stale"}
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
onclick={jumpToMapping}
|
||||
>
|
||||
{$t.dataset_review?.preview?.review_inputs_action}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if localMessage}
|
||||
<div
|
||||
class={`mt-4 rounded-xl px-3 py-3 text-sm ${
|
||||
localStatus === "error"
|
||||
? "border border-red-200 bg-red-50 text-red-700"
|
||||
: "border border-emerald-200 bg-emerald-50 text-emerald-800"
|
||||
}`}
|
||||
>
|
||||
{localMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- [/DEF:CompiledSQLPreview:Component] -->
|
||||
Reference in New Issue
Block a user