feat(us1): add dataset review orchestration automatic review slice
This commit is contained in:
0
frontend/src/lib/components/dataset-review/.gitkeep
Normal file
0
frontend/src/lib/components/dataset-review/.gitkeep
Normal file
@@ -0,0 +1,332 @@
|
||||
<!-- [DEF:SourceIntakePanel:Component] -->
|
||||
<!-- @COMPLEXITY: 3 -->
|
||||
<!-- @SEMANTICS: dataset-review, intake, superset-link, dataset-selection, validation -->
|
||||
<!-- @PURPOSE: Collect initial dataset source input through Superset link paste or dataset selection entry paths. -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[api_module] -->
|
||||
<!-- @UX_STATE: Idle -> Empty intake form with two clear entry paths. -->
|
||||
<!-- @UX_STATE: Validating -> Lightweight inline validation feedback. -->
|
||||
<!-- @UX_STATE: Rejected -> Input error shown with corrective hint. -->
|
||||
<!-- @UX_FEEDBACK: Recognized links are acknowledged before deeper recovery finishes. -->
|
||||
<!-- @UX_RECOVERY: Users can correct invalid input in place without resetting the page. -->
|
||||
<!-- @UX_REACTIVITY: Uses $props, $state, and $derived only; no legacy reactive syntax. -->
|
||||
<script>
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
let {
|
||||
environments = [],
|
||||
selectedEnvironmentId = "",
|
||||
initialSourceKind = "superset_link",
|
||||
initialSourceInput = "",
|
||||
submitting = false,
|
||||
acknowledgment = "",
|
||||
onsubmit = async () => {},
|
||||
} = $props();
|
||||
|
||||
let sourceKind = $state("superset_link");
|
||||
let sourceInput = $state("");
|
||||
let environmentId = $state("");
|
||||
let touched = $state(false);
|
||||
let localError = $state("");
|
||||
let submitAttempted = $state(false);
|
||||
|
||||
const normalizedSourceInput = $derived(sourceInput.trim());
|
||||
const normalizedEnvironmentId = $derived(String(environmentId || "").trim());
|
||||
const isSupersetLinkMode = $derived(sourceKind === "superset_link");
|
||||
const linkLooksRecognized = $derived(
|
||||
/^https?:\/\/.+\/superset\/|^https?:\/\/.+\/explore\/|^https?:\/\/.+\/dashboard\//i.test(
|
||||
normalizedSourceInput,
|
||||
),
|
||||
);
|
||||
const intakeState = $derived(
|
||||
localError
|
||||
? "Rejected"
|
||||
: submitting || touched
|
||||
? "Validating"
|
||||
: "Idle",
|
||||
);
|
||||
const canSubmit = $derived(
|
||||
normalizedEnvironmentId.length > 0 && normalizedSourceInput.length > 0 && !submitting,
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!touched && !submitAttempted) {
|
||||
sourceKind = initialSourceKind || "superset_link";
|
||||
}
|
||||
|
||||
if (!environmentId) {
|
||||
environmentId = selectedEnvironmentId || "";
|
||||
}
|
||||
|
||||
if (!sourceInput) {
|
||||
sourceInput = initialSourceInput || "";
|
||||
}
|
||||
});
|
||||
|
||||
function setMode(nextMode) {
|
||||
sourceKind = nextMode;
|
||||
localError = "";
|
||||
touched = false;
|
||||
submitAttempted = false;
|
||||
}
|
||||
|
||||
function getInlineHint() {
|
||||
if (!normalizedEnvironmentId) {
|
||||
return $t.dataset_review?.source?.environment_required;
|
||||
}
|
||||
|
||||
if (!normalizedSourceInput) {
|
||||
return isSupersetLinkMode
|
||||
? $t.dataset_review?.source?.superset_link_hint
|
||||
: $t.dataset_review?.source?.dataset_selection_hint;
|
||||
}
|
||||
|
||||
if (localError) {
|
||||
return localError;
|
||||
}
|
||||
|
||||
if (isSupersetLinkMode && linkLooksRecognized) {
|
||||
return (
|
||||
acknowledgment ||
|
||||
$t.dataset_review?.source?.recognized_link_hint ||
|
||||
$t.dataset_review?.source?.superset_link_hint
|
||||
);
|
||||
}
|
||||
|
||||
return isSupersetLinkMode
|
||||
? $t.dataset_review?.source?.superset_link_hint
|
||||
: $t.dataset_review?.source?.dataset_selection_hint;
|
||||
}
|
||||
|
||||
function validatePayload() {
|
||||
if (!normalizedEnvironmentId) {
|
||||
return $t.dataset_review?.source?.environment_required;
|
||||
}
|
||||
|
||||
if (!normalizedSourceInput) {
|
||||
return isSupersetLinkMode
|
||||
? $t.dataset_review?.source?.superset_link_required
|
||||
: $t.dataset_review?.source?.dataset_selection_required;
|
||||
}
|
||||
|
||||
if (isSupersetLinkMode && !/^https?:\/\//i.test(normalizedSourceInput)) {
|
||||
return $t.dataset_review?.source?.superset_link_invalid;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function submitPayload() {
|
||||
touched = true;
|
||||
submitAttempted = true;
|
||||
localError = validatePayload();
|
||||
|
||||
if (localError) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onsubmit({
|
||||
source_kind: sourceKind,
|
||||
source_input: normalizedSourceInput,
|
||||
environment_id: normalizedEnvironmentId,
|
||||
});
|
||||
localError = "";
|
||||
} catch (error) {
|
||||
localError =
|
||||
error?.message ||
|
||||
$t.dataset_review?.source?.submit_failed ||
|
||||
$t.common?.error;
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
touched = true;
|
||||
if (submitAttempted) {
|
||||
localError = validatePayload();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSupersetLink(event) {
|
||||
event?.preventDefault?.();
|
||||
sourceKind = "superset_link";
|
||||
await submitPayload();
|
||||
}
|
||||
|
||||
async function submitDatasetSelection(event) {
|
||||
event?.preventDefault?.();
|
||||
sourceKind = "dataset_selection";
|
||||
await submitPayload();
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
if (isSupersetLinkMode) {
|
||||
await submitSupersetLink(event);
|
||||
return;
|
||||
}
|
||||
|
||||
await submitDatasetSelection(event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-blue-700">
|
||||
{$t.dataset_review?.source?.eyebrow}
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-slate-900">
|
||||
{$t.dataset_review?.source?.title}
|
||||
</h2>
|
||||
<p class="mt-1 max-w-2xl text-sm text-slate-600">
|
||||
{$t.dataset_review?.source?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${
|
||||
intakeState === "Rejected"
|
||||
? "bg-red-100 text-red-700"
|
||||
: intakeState === "Validating"
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-slate-100 text-slate-700"
|
||||
}`}
|
||||
>
|
||||
{#if intakeState === "Rejected"}
|
||||
{$t.dataset_review?.source?.state_rejected}
|
||||
{:else if intakeState === "Validating"}
|
||||
{$t.dataset_review?.source?.state_validating}
|
||||
{:else}
|
||||
{$t.dataset_review?.source?.state_idle}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 grid gap-3 md:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-xl border px-4 py-3 text-left transition ${
|
||||
isSupersetLinkMode
|
||||
? "border-blue-500 bg-blue-50 text-blue-900 shadow-sm"
|
||||
: "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
|
||||
}`}
|
||||
onclick={() => setMode("superset_link")}
|
||||
>
|
||||
<div class="text-sm font-semibold">
|
||||
{$t.dataset_review?.source?.superset_link_tab}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
{$t.dataset_review?.source?.superset_link_tab_hint}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-xl border px-4 py-3 text-left transition ${
|
||||
!isSupersetLinkMode
|
||||
? "border-blue-500 bg-blue-50 text-blue-900 shadow-sm"
|
||||
: "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
|
||||
}`}
|
||||
onclick={() => setMode("dataset_selection")}
|
||||
>
|
||||
<div class="text-sm font-semibold">
|
||||
{$t.dataset_review?.source?.dataset_selection_tab}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
{$t.dataset_review?.source?.dataset_selection_tab_hint}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form class="mt-5 space-y-4" onsubmit={handleSubmit}>
|
||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,14rem)_minmax(0,1fr)]">
|
||||
<label class="space-y-2">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
{$t.dataset_review?.source?.environment_label}
|
||||
</span>
|
||||
<select
|
||||
class="w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
||||
bind:value={environmentId}
|
||||
onchange={handleInput}
|
||||
>
|
||||
<option value="">{$t.common?.choose_environment}</option>
|
||||
{#each environments as environment}
|
||||
<option value={environment.id}>{environment.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="space-y-2">
|
||||
<span class="text-sm font-medium text-slate-700">
|
||||
{isSupersetLinkMode
|
||||
? $t.dataset_review?.source?.superset_link_label
|
||||
: $t.dataset_review?.source?.dataset_selection_label}
|
||||
</span>
|
||||
|
||||
{#if isSupersetLinkMode}
|
||||
<input
|
||||
class={`w-full rounded-xl border px-3 py-2 text-sm text-slate-900 shadow-sm outline-none transition focus:ring-2 ${
|
||||
localError
|
||||
? "border-red-300 bg-red-50 focus:border-red-400 focus:ring-red-100"
|
||||
: "border-slate-300 bg-white focus:border-blue-500 focus:ring-blue-200"
|
||||
}`}
|
||||
type="url"
|
||||
placeholder={$t.dataset_review?.source?.superset_link_placeholder}
|
||||
bind:value={sourceInput}
|
||||
oninput={handleInput}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
class={`w-full rounded-xl border px-3 py-2 text-sm text-slate-900 shadow-sm outline-none transition focus:ring-2 ${
|
||||
localError
|
||||
? "border-red-300 bg-red-50 focus:border-red-400 focus:ring-red-100"
|
||||
: "border-slate-300 bg-white focus:border-blue-500 focus:ring-blue-200"
|
||||
}`}
|
||||
type="text"
|
||||
placeholder={$t.dataset_review?.source?.dataset_selection_placeholder}
|
||||
bind:value={sourceInput}
|
||||
oninput={handleInput}
|
||||
/>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`rounded-xl border px-4 py-3 text-sm ${
|
||||
localError
|
||||
? "border-red-200 bg-red-50 text-red-700"
|
||||
: linkLooksRecognized && isSupersetLinkMode
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-800"
|
||||
: "border-slate-200 bg-slate-50 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{getInlineHint()}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-xs text-slate-500">
|
||||
{#if isSupersetLinkMode}
|
||||
{$t.dataset_review?.source?.superset_link_recovery_note}
|
||||
{:else}
|
||||
{$t.dataset_review?.source?.dataset_selection_recovery_note}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{#if submitting}
|
||||
{$t.dataset_review?.source?.submitting}
|
||||
{:else if isSupersetLinkMode}
|
||||
{$t.dataset_review?.source?.submit_superset_link}
|
||||
{:else}
|
||||
{$t.dataset_review?.source?.submit_dataset_selection}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:SourceIntakePanel:Component] -->
|
||||
@@ -0,0 +1,332 @@
|
||||
<!-- [DEF:ValidationFindingsPanel:Component] -->
|
||||
<!-- @COMPLEXITY: 3 -->
|
||||
<!-- @SEMANTICS: dataset-review, findings, severity, readiness, actionability -->
|
||||
<!-- @PURPOSE: Present validation findings grouped by severity with explicit resolution and actionability signals. -->
|
||||
<!-- @LAYER: UI -->
|
||||
<!-- @RELATION: [BINDS_TO] ->[DatasetReviewWorkspace] -->
|
||||
<!-- @UX_STATE: Blocking -> Blocking findings are visually dominant and block launch flow. -->
|
||||
<!-- @UX_STATE: Warning -> Warnings remain visible with explicit approval or defer actions. -->
|
||||
<!-- @UX_STATE: Informational -> Low-priority findings are collapsed or secondary. -->
|
||||
<!-- @UX_FEEDBACK: Resolving or approving an item updates readiness state immediately. -->
|
||||
<!-- @UX_RECOVERY: Users can jump from a finding directly to the relevant remediation area. -->
|
||||
<!-- @UX_REACTIVITY: Uses $props and $derived only; no legacy reactive syntax. -->
|
||||
|
||||
<script>
|
||||
import { t } from "$lib/i18n";
|
||||
|
||||
let {
|
||||
findings = [],
|
||||
recommendedAction = "",
|
||||
onjump = () => {},
|
||||
} = $props();
|
||||
|
||||
const groupedFindings = $derived(groupFindingsBySeverity(findings || []));
|
||||
const blockingFindings = $derived(groupedFindings.blocking);
|
||||
const warningFindings = $derived(groupedFindings.warning);
|
||||
const informationalFindings = $derived(groupedFindings.informational);
|
||||
const totalFindings = $derived(
|
||||
blockingFindings.length + warningFindings.length + informationalFindings.length,
|
||||
);
|
||||
|
||||
function groupFindingsBySeverity(items) {
|
||||
return (items || []).reduce(
|
||||
(accumulator, finding) => {
|
||||
const severity = String(finding?.severity || "informational");
|
||||
if (severity === "blocking") {
|
||||
accumulator.blocking.push(finding);
|
||||
return accumulator;
|
||||
}
|
||||
if (severity === "warning") {
|
||||
accumulator.warning.push(finding);
|
||||
return accumulator;
|
||||
}
|
||||
accumulator.informational.push(finding);
|
||||
return accumulator;
|
||||
},
|
||||
{ blocking: [], warning: [], informational: [] },
|
||||
);
|
||||
}
|
||||
|
||||
function buildTargetFromFinding(finding) {
|
||||
const area = String(finding?.area || "");
|
||||
if (area === "source_intake") return "intake";
|
||||
if (area === "dataset_profile") return "summary";
|
||||
if (area === "semantic_enrichment") return "semantics";
|
||||
if (area === "clarification") return "clarification";
|
||||
if (area === "filter_recovery") return "filters";
|
||||
if (area === "template_mapping") return "mappings";
|
||||
if (area === "compiled_preview") return "preview";
|
||||
if (area === "launch") return "launch";
|
||||
return "summary";
|
||||
}
|
||||
|
||||
function jumpToFindingTarget(finding) {
|
||||
onjump({
|
||||
target: buildTargetFromFinding(finding),
|
||||
finding,
|
||||
});
|
||||
}
|
||||
|
||||
function getResolutionLabel(resolutionState) {
|
||||
const normalized = String(resolutionState || "open");
|
||||
return $t.dataset_review?.findings?.resolution?.[normalized] || normalized;
|
||||
}
|
||||
|
||||
function getAreaLabel(area) {
|
||||
const normalized = String(area || "dataset_profile");
|
||||
return $t.dataset_review?.findings?.areas?.[normalized] || normalized;
|
||||
}
|
||||
|
||||
function getRecommendedActionLabel(action) {
|
||||
const normalized = String(action || "");
|
||||
return $t.dataset_review?.workspace?.actions?.[normalized] || normalized;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div 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?.findings?.eyebrow}
|
||||
</p>
|
||||
<h2 class="text-xl font-semibold text-slate-900">
|
||||
{$t.dataset_review?.findings?.title}
|
||||
</h2>
|
||||
<p class="mt-1 max-w-3xl text-sm text-slate-600">
|
||||
{$t.dataset_review?.findings?.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-right">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
{$t.dataset_review?.findings?.next_action_label}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-slate-900">
|
||||
{getRecommendedActionLabel(recommendedAction)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if totalFindings === 0}
|
||||
<div class="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-4 text-sm text-emerald-800">
|
||||
{$t.dataset_review?.findings?.empty}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-5 space-y-4">
|
||||
<section class="rounded-2xl border border-red-200 bg-red-50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-sm font-semibold text-slate-900">
|
||||
{$t.dataset_review?.findings?.blocking_title}
|
||||
</h3>
|
||||
<span class="rounded-full bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700">
|
||||
{blockingFindings.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if blockingFindings.length === 0}
|
||||
<p class="mt-3 text-sm text-slate-600">
|
||||
{$t.dataset_review?.findings?.blocking_empty}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each blockingFindings as finding}
|
||||
<article class="rounded-xl border border-white/70 bg-white/80 p-4 shadow-sm">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-900">
|
||||
{finding.title}
|
||||
</h4>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">
|
||||
{finding.code}
|
||||
</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600">
|
||||
{getAreaLabel(finding.area)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm leading-6 text-slate-700">
|
||||
{finding.message}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.resolution_label}:
|
||||
{getResolutionLabel(finding.resolution_state)}
|
||||
</span>
|
||||
|
||||
{#if finding.caused_by_ref}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.reference_label}:
|
||||
{finding.caused_by_ref}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if finding.resolution_note}
|
||||
<p class="mt-2 text-xs text-slate-500">{finding.resolution_note}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
onclick={() => jumpToFindingTarget(finding)}
|
||||
>
|
||||
{$t.dataset_review?.findings?.jump_action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-sm font-semibold text-slate-900">
|
||||
{$t.dataset_review?.findings?.warning_title}
|
||||
</h3>
|
||||
<span class="rounded-full bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-700">
|
||||
{warningFindings.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if warningFindings.length === 0}
|
||||
<p class="mt-3 text-sm text-slate-600">
|
||||
{$t.dataset_review?.findings?.warning_empty}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each warningFindings as finding}
|
||||
<article class="rounded-xl border border-white/70 bg-white/80 p-4 shadow-sm">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-900">
|
||||
{finding.title}
|
||||
</h4>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">
|
||||
{finding.code}
|
||||
</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600">
|
||||
{getAreaLabel(finding.area)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm leading-6 text-slate-700">
|
||||
{finding.message}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.resolution_label}:
|
||||
{getResolutionLabel(finding.resolution_state)}
|
||||
</span>
|
||||
|
||||
{#if finding.caused_by_ref}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.reference_label}:
|
||||
{finding.caused_by_ref}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if finding.resolution_note}
|
||||
<p class="mt-2 text-xs text-slate-500">{finding.resolution_note}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
onclick={() => jumpToFindingTarget(finding)}
|
||||
>
|
||||
{$t.dataset_review?.findings?.jump_action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-sm font-semibold text-slate-900">
|
||||
{$t.dataset_review?.findings?.informational_title}
|
||||
</h3>
|
||||
<span class="rounded-full bg-slate-200 px-2.5 py-1 text-xs font-medium text-slate-700">
|
||||
{informationalFindings.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if informationalFindings.length === 0}
|
||||
<p class="mt-3 text-sm text-slate-600">
|
||||
{$t.dataset_review?.findings?.informational_empty}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each informationalFindings as finding}
|
||||
<article class="rounded-xl border border-white/70 bg-white/80 p-4 shadow-sm">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-900">
|
||||
{finding.title}
|
||||
</h4>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">
|
||||
{finding.code}
|
||||
</span>
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-slate-600">
|
||||
{getAreaLabel(finding.area)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm leading-6 text-slate-700">
|
||||
{finding.message}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.resolution_label}:
|
||||
{getResolutionLabel(finding.resolution_state)}
|
||||
</span>
|
||||
|
||||
{#if finding.caused_by_ref}
|
||||
<span class="rounded-full bg-slate-100 px-2 py-1 text-slate-600">
|
||||
{$t.dataset_review?.findings?.reference_label}:
|
||||
{finding.caused_by_ref}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if finding.resolution_note}
|
||||
<p class="mt-2 text-xs text-slate-500">{finding.resolution_note}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100"
|
||||
onclick={() => jumpToFindingTarget(finding)}
|
||||
>
|
||||
{$t.dataset_review?.findings?.jump_action}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:ValidationFindingsPanel:Component] -->
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
// [DEF:SourceIntakePanelUxTests:Module]
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: dataset-review, source-intake, ux-tests, validation, recovery
|
||||
// @PURPOSE: Verify source intake entry paths, validation feedback, and submit payload behavior for US1.
|
||||
// @LAYER: UI
|
||||
// @RELATION: [VERIFIES] ->[SourceIntakePanel]
|
||||
// @UX_STATE: Idle -> Default intake renders both entry paths and waits for user input.
|
||||
// @UX_STATE: Validating -> Inline feedback confirms recognized links and local payload validation.
|
||||
// @UX_STATE: Rejected -> Invalid source input remains local and exposes recovery guidance.
|
||||
// @TEST_CONTRACT: SourceIntakePanelProps -> ObservableIntakeUX
|
||||
// @TEST_SCENARIO: invalid_superset_link_shows_rejected_state -> Invalid URL input keeps submit local and shows rejection feedback.
|
||||
// @TEST_SCENARIO: recognized_superset_link_submits_payload -> Recognized link path submits normalized payload.
|
||||
// @TEST_SCENARIO: dataset_selection_mode_changes_cta -> Dataset selection path switches CTA and payload source kind.
|
||||
// @TEST_EDGE: missing_environment -> Required environment guard blocks submit.
|
||||
// @TEST_EDGE: invalid_type -> Invalid Superset URL shows recovery hint.
|
||||
// @TEST_EDGE: external_fail -> Submit callback failure is rendered inline.
|
||||
// @TEST_INVARIANT: intake_contract_remains_observable -> VERIFIED_BY: [invalid_superset_link_shows_rejected_state, recognized_superset_link_submits_payload, dataset_selection_mode_changes_cta]
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
||||
import SourceIntakePanel from "../SourceIntakePanel.svelte";
|
||||
|
||||
vi.mock("$lib/i18n", () => ({
|
||||
t: {
|
||||
subscribe: (fn) => {
|
||||
fn({
|
||||
common: {
|
||||
choose_environment: "Choose environment",
|
||||
error: "Common error",
|
||||
},
|
||||
dataset_review: {
|
||||
source: {
|
||||
eyebrow: "Source intake",
|
||||
title: "Start dataset review",
|
||||
description: "Paste link or provide dataset reference.",
|
||||
state_idle: "Idle",
|
||||
state_validating: "Validating",
|
||||
state_rejected: "Rejected",
|
||||
environment_label: "Environment",
|
||||
environment_required: "Environment is required",
|
||||
superset_link_tab: "Superset link",
|
||||
superset_link_tab_hint: "Paste dashboard or explore URL",
|
||||
dataset_selection_tab: "Dataset selection",
|
||||
dataset_selection_tab_hint: "Enter dataset ref",
|
||||
superset_link_label: "Superset link",
|
||||
dataset_selection_label: "Dataset reference",
|
||||
superset_link_placeholder: "https://superset.local/dashboard/10",
|
||||
dataset_selection_placeholder: "public.sales",
|
||||
superset_link_hint: "Paste a full Superset URL",
|
||||
dataset_selection_hint: "Provide schema.dataset reference",
|
||||
recognized_link_hint: "Recognized Superset link",
|
||||
superset_link_required: "Superset link is required",
|
||||
dataset_selection_required: "Dataset reference is required",
|
||||
superset_link_invalid: "Superset link must start with http",
|
||||
submit_failed: "Submit failed",
|
||||
superset_link_recovery_note: "You can fix the link inline",
|
||||
dataset_selection_recovery_note: "You can fix the dataset inline",
|
||||
submitting: "Submitting",
|
||||
submit_superset_link: "Start from link",
|
||||
submit_dataset_selection: "Start from dataset",
|
||||
dataset_selection_acknowledged: "Dataset selection acknowledged",
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("SourceIntakePanel UX Contract", () => {
|
||||
const environments = [{ id: "env-1", name: "DEV" }];
|
||||
|
||||
it("invalid_superset_link_shows_rejected_state", async () => {
|
||||
const onsubmit = vi.fn();
|
||||
const { container } = render(SourceIntakePanel, {
|
||||
environments,
|
||||
onsubmit,
|
||||
});
|
||||
|
||||
await fireEvent.change(screen.getByRole("combobox"), {
|
||||
target: { value: "env-1" },
|
||||
});
|
||||
await fireEvent.input(screen.getByPlaceholderText("https://superset.local/dashboard/10"), {
|
||||
target: { value: "not-a-url" },
|
||||
});
|
||||
await fireEvent.submit(container.querySelector("form"));
|
||||
|
||||
expect(onsubmit).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("Rejected")).toBeDefined();
|
||||
expect(screen.getByText("Superset link must start with http")).toBeDefined();
|
||||
});
|
||||
|
||||
it("recognized_superset_link_submits_payload", async () => {
|
||||
const onsubmit = vi.fn().mockResolvedValue(undefined);
|
||||
const { container } = render(SourceIntakePanel, {
|
||||
environments,
|
||||
selectedEnvironmentId: "env-1",
|
||||
acknowledgment: "Recognized Superset link",
|
||||
onsubmit,
|
||||
});
|
||||
|
||||
await fireEvent.input(screen.getByPlaceholderText("https://superset.local/dashboard/10"), {
|
||||
target: { value: "https://demo.local/superset/dashboard/42 " },
|
||||
});
|
||||
await fireEvent.submit(container.querySelector("form"));
|
||||
|
||||
expect(onsubmit).toHaveBeenCalledWith({
|
||||
environment_id: "env-1",
|
||||
source_input: "https://demo.local/superset/dashboard/42",
|
||||
source_kind: "superset_link",
|
||||
});
|
||||
expect(screen.getByText("Recognized Superset link")).toBeDefined();
|
||||
});
|
||||
|
||||
it("dataset_selection_mode_changes_cta", async () => {
|
||||
const onsubmit = vi.fn().mockResolvedValue(undefined);
|
||||
const { container } = render(SourceIntakePanel, {
|
||||
environments,
|
||||
onsubmit,
|
||||
});
|
||||
|
||||
await fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "Dataset selection Enter dataset ref",
|
||||
}),
|
||||
);
|
||||
await fireEvent.change(screen.getByRole("combobox"), {
|
||||
target: { value: "env-1" },
|
||||
});
|
||||
await fireEvent.input(screen.getByPlaceholderText("public.sales"), {
|
||||
target: { value: " public.sales " },
|
||||
});
|
||||
await fireEvent.submit(container.querySelector("form"));
|
||||
|
||||
expect(onsubmit).toHaveBeenCalledWith({
|
||||
environment_id: "env-1",
|
||||
source_input: "public.sales",
|
||||
source_kind: "dataset_selection",
|
||||
});
|
||||
});
|
||||
|
||||
it("external_fail_renders_inline_error", async () => {
|
||||
const onsubmit = vi.fn().mockRejectedValue(new Error("Backend rejected source"));
|
||||
const { container } = render(SourceIntakePanel, {
|
||||
environments,
|
||||
selectedEnvironmentId: "env-1",
|
||||
onsubmit,
|
||||
});
|
||||
|
||||
await fireEvent.input(screen.getByPlaceholderText("https://superset.local/dashboard/10"), {
|
||||
target: { value: "https://demo.local/dashboard/42" },
|
||||
});
|
||||
await fireEvent.submit(container.querySelector("form"));
|
||||
|
||||
expect(screen.getByText("Backend rejected source")).toBeDefined();
|
||||
});
|
||||
});
|
||||
// [/DEF:SourceIntakePanelUxTests:Module]
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
// [DEF:ValidationFindingsPanelUxTests:Module]
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: dataset-review, findings, severity, jump, ux-tests
|
||||
// @PURPOSE: Verify grouped findings visibility, empty state, and remediation jump behavior for US1.
|
||||
// @LAYER: UI
|
||||
// @RELATION: [VERIFIES] ->[ValidationFindingsPanel]
|
||||
// @UX_STATE: Blocking -> Blocking findings dominate the panel and expose remediation jumps.
|
||||
// @UX_STATE: Warning -> Warning findings remain visible with explicit resolution state.
|
||||
// @UX_STATE: Informational -> Informational notes stay readable without competing with blockers.
|
||||
// @TEST_CONTRACT: FindingsPanelProps -> ObservableFindingsUX
|
||||
// @TEST_SCENARIO: blocking_warning_info_groups_render -> Findings render in the proper severity groups with counts.
|
||||
// @TEST_SCENARIO: jump_action_maps_area_to_workspace_target -> Jump action emits normalized remediation target.
|
||||
// @TEST_SCENARIO: empty_findings_show_success_state -> Empty list shows ready feedback.
|
||||
// @TEST_EDGE: missing_field -> Missing optional fields do not crash rendering.
|
||||
// @TEST_EDGE: invalid_type -> Unknown severity falls back to informational grouping.
|
||||
// @TEST_EDGE: external_fail -> Jump callback remains optional.
|
||||
// @TEST_INVARIANT: findings_groups_remain_actionable -> VERIFIED_BY: [blocking_warning_info_groups_render, jump_action_maps_area_to_workspace_target, empty_findings_show_success_state]
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/svelte";
|
||||
import ValidationFindingsPanel from "../ValidationFindingsPanel.svelte";
|
||||
|
||||
vi.mock("$lib/i18n", () => ({
|
||||
t: {
|
||||
subscribe: (fn) => {
|
||||
fn({
|
||||
dataset_review: {
|
||||
findings: {
|
||||
eyebrow: "Validation findings",
|
||||
title: "Findings",
|
||||
description: "Review validation results",
|
||||
next_action_label: "Next action",
|
||||
empty: "No findings",
|
||||
blocking_title: "Blocking",
|
||||
blocking_empty: "No blocking findings",
|
||||
warning_title: "Warnings",
|
||||
warning_empty: "No warning findings",
|
||||
informational_title: "Info",
|
||||
informational_empty: "No informational findings",
|
||||
resolution_label: "Resolution",
|
||||
reference_label: "Reference",
|
||||
jump_action: "Jump to area",
|
||||
resolution: {
|
||||
open: "Open",
|
||||
approved: "Approved",
|
||||
},
|
||||
areas: {
|
||||
source_intake: "Source intake",
|
||||
dataset_profile: "Dataset summary",
|
||||
semantic_enrichment: "Semantics",
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
actions: {
|
||||
review_documentation: "Review documentation",
|
||||
import_from_superset: "Import from Superset",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ValidationFindingsPanel UX Contract", () => {
|
||||
const findings = [
|
||||
{
|
||||
finding_id: "f-1",
|
||||
severity: "blocking",
|
||||
code: "REQ_ENV",
|
||||
area: "source_intake",
|
||||
title: "Environment required",
|
||||
message: "Select environment",
|
||||
resolution_state: "open",
|
||||
},
|
||||
{
|
||||
finding_id: "f-2",
|
||||
severity: "warning",
|
||||
code: "PARTIAL_RECOVERY",
|
||||
area: "dataset_profile",
|
||||
title: "Partial recovery",
|
||||
message: "Some metadata needs review",
|
||||
resolution_state: "approved",
|
||||
caused_by_ref: "dashboard:42",
|
||||
},
|
||||
{
|
||||
finding_id: "f-3",
|
||||
severity: "unexpected",
|
||||
code: "INFO_NOTE",
|
||||
area: "semantic_enrichment",
|
||||
title: "Dictionary note",
|
||||
message: "Trusted source used",
|
||||
resolution_state: "open",
|
||||
},
|
||||
];
|
||||
|
||||
it("blocking_warning_info_groups_render", () => {
|
||||
render(ValidationFindingsPanel, {
|
||||
findings,
|
||||
recommendedAction: "review_documentation",
|
||||
});
|
||||
|
||||
expect(screen.getByText("Blocking")).toBeDefined();
|
||||
expect(screen.getByText("Warnings")).toBeDefined();
|
||||
expect(screen.getByText("Info")).toBeDefined();
|
||||
expect(screen.getByText("Environment required")).toBeDefined();
|
||||
expect(screen.getByText("Partial recovery")).toBeDefined();
|
||||
expect(screen.getByText("Dictionary note")).toBeDefined();
|
||||
expect(screen.getByText("Review documentation")).toBeDefined();
|
||||
});
|
||||
|
||||
it("jump_action_maps_area_to_workspace_target", async () => {
|
||||
const onjump = vi.fn();
|
||||
render(ValidationFindingsPanel, {
|
||||
findings: [findings[0]],
|
||||
recommendedAction: "import_from_superset",
|
||||
onjump,
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByRole("button", { name: "Jump to area" }));
|
||||
|
||||
expect(onjump).toHaveBeenCalledWith({
|
||||
target: "intake",
|
||||
finding: findings[0],
|
||||
});
|
||||
});
|
||||
|
||||
it("empty_findings_show_success_state", () => {
|
||||
render(ValidationFindingsPanel, {
|
||||
findings: [],
|
||||
recommendedAction: "review_documentation",
|
||||
});
|
||||
|
||||
expect(screen.getByText("No findings")).toBeDefined();
|
||||
});
|
||||
});
|
||||
// [/DEF:ValidationFindingsPanelUxTests:Module]
|
||||
@@ -586,6 +586,187 @@
|
||||
"task_failed": "Failed",
|
||||
"task_waiting": "Waiting"
|
||||
},
|
||||
"dataset_review": {
|
||||
"source": {
|
||||
"eyebrow": "Source intake",
|
||||
"title": "Start dataset review",
|
||||
"description": "Paste a Superset link or provide a dataset reference to begin a resumable review session.",
|
||||
"state_idle": "Idle",
|
||||
"state_validating": "Validating",
|
||||
"state_rejected": "Rejected",
|
||||
"environment_label": "Environment",
|
||||
"environment_required": "Select the Superset environment before starting review.",
|
||||
"superset_link_tab": "Superset link",
|
||||
"superset_link_tab_hint": "Import dashboard or explore context and recover filters progressively.",
|
||||
"dataset_selection_tab": "Dataset selection",
|
||||
"dataset_selection_tab_hint": "Start from a known dataset reference when you already know the target.",
|
||||
"superset_link_label": "Superset URL",
|
||||
"superset_link_placeholder": "https://superset.example.com/superset/dashboard/42/?native_filters=...",
|
||||
"superset_link_hint": "Recognized Superset links create a session immediately, then continue partial recovery in place.",
|
||||
"superset_link_required": "Paste a Superset link to continue.",
|
||||
"superset_link_invalid": "Enter a full Superset URL starting with http:// or https://.",
|
||||
"superset_link_recovery_note": "Partial recovery stays visible. Missing filters or unresolved context are disclosed instead of hidden.",
|
||||
"recognized_link_hint": "Superset link recognized. Session recovery will continue after the shell is created.",
|
||||
"dataset_selection_label": "Dataset reference",
|
||||
"dataset_selection_placeholder": "dataset_id:123 or sales.daily_revenue",
|
||||
"dataset_selection_hint": "Use a dataset id or stable dataset reference from the selected environment.",
|
||||
"dataset_selection_required": "Enter a dataset reference to continue.",
|
||||
"dataset_selection_recovery_note": "Dataset-based intake still surfaces confidence and unresolved context explicitly.",
|
||||
"dataset_selection_acknowledged": "Dataset reference accepted. Loading the resumable review shell.",
|
||||
"submit_superset_link": "Start from link",
|
||||
"submit_dataset_selection": "Start from dataset",
|
||||
"submitting": "Starting review...",
|
||||
"submit_failed": "Unable to start the dataset review session."
|
||||
},
|
||||
"findings": {
|
||||
"eyebrow": "Validation findings",
|
||||
"title": "Review blockers, warnings, and informational notes",
|
||||
"description": "Findings stay grouped by severity so the highest-risk issues remain visible while partial recovery stays usable.",
|
||||
"next_action_label": "Recommended next step",
|
||||
"empty": "No findings recorded yet. The session can continue with the current recovered context.",
|
||||
"blocking_title": "Blocking",
|
||||
"blocking_empty": "No blocking findings are currently open.",
|
||||
"warning_title": "Warnings",
|
||||
"warning_empty": "No warning-level findings need attention right now.",
|
||||
"informational_title": "Informational",
|
||||
"informational_empty": "No informational findings are currently recorded.",
|
||||
"resolution_label": "Resolution",
|
||||
"reference_label": "Reference",
|
||||
"jump_action": "Open related area",
|
||||
"resolution": {
|
||||
"open": "Open",
|
||||
"resolved": "Resolved",
|
||||
"approved": "Approved",
|
||||
"skipped": "Skipped",
|
||||
"deferred": "Deferred",
|
||||
"expert_review": "Expert review"
|
||||
},
|
||||
"areas": {
|
||||
"source_intake": "Source intake",
|
||||
"dataset_profile": "Dataset profile",
|
||||
"semantic_enrichment": "Semantic enrichment",
|
||||
"clarification": "Clarification",
|
||||
"filter_recovery": "Filter recovery",
|
||||
"template_mapping": "Template mapping",
|
||||
"compiled_preview": "Compiled preview",
|
||||
"launch": "Launch",
|
||||
"audit": "Audit"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"eyebrow": "Dataset orchestration",
|
||||
"title": "Dataset review workspace",
|
||||
"description": "Resume a prior session or inspect recovered context, findings, provenance, and exports in one place.",
|
||||
"loading": "Loading review workspace...",
|
||||
"load_failed": "Failed to load the dataset review session.",
|
||||
"resume_failed": "Failed to resume the current review session.",
|
||||
"save_failed": "Failed to update the review session.",
|
||||
"export_failed": "Failed to export the requested review artifact.",
|
||||
"empty_state_title": "No session loaded",
|
||||
"state_label": "Workspace",
|
||||
"readiness_label": "Readiness",
|
||||
"source_badge_fallback": "review session",
|
||||
"save_session_action": "Save session",
|
||||
"export_summary_action": "Export summary",
|
||||
"save_summary_action": "Save summary",
|
||||
"summary_edit_saved": "Summary draft updated for the current review workspace.",
|
||||
"import_progress_eyebrow": "Import progress",
|
||||
"import_progress_title": "Recovering dataset context progressively",
|
||||
"import_progress_body": "The workspace reveals milestones as source, filters, variables, and semantic candidates become available.",
|
||||
"import_milestones": {
|
||||
"recognized": "Dataset recognized",
|
||||
"filters": "Saved native filters recovered",
|
||||
"variables": "Dataset template variables detected",
|
||||
"semantic_sources": "Nearby semantic sources identified",
|
||||
"summary": "Preliminary summary prepared"
|
||||
},
|
||||
"source_session_title": "Source & session",
|
||||
"source_label": "Dataset source",
|
||||
"import_status_title": "Superset import status",
|
||||
"recent_actions_title": "Recent actions timeline",
|
||||
"timeline": {
|
||||
"source": "Source accepted",
|
||||
"status": "Session state updated",
|
||||
"filters": "Recovered filters",
|
||||
"findings": "Validation findings recorded",
|
||||
"exports": "Latest export generated"
|
||||
},
|
||||
"next_action_label": "Next action",
|
||||
"next_action_card_eyebrow": "Primary action",
|
||||
"next_action_card_body": "The main action always reflects the highest-value next step for the current session state.",
|
||||
"session_label": "Session summary",
|
||||
"session_id_label": "Session ID",
|
||||
"summary_source_label": "Summary source",
|
||||
"confidence_label": "Confidence",
|
||||
"business_summary_title": "Business summary",
|
||||
"summary_missing": "No readable business summary is available yet.",
|
||||
"partial_recovery_badge": "Partial recovery",
|
||||
"phase_label": "Phase",
|
||||
"status_label": "Status",
|
||||
"active_task_label": "Active task",
|
||||
"recovery_title": "Recovered filters and provenance",
|
||||
"recovery_empty": "No imported filters have been recovered yet.",
|
||||
"provenance_label": "Provenance",
|
||||
"recovered_value_label": "Recovered value",
|
||||
"health_title": "Findings overview",
|
||||
"open_findings_label": "Open findings",
|
||||
"exports_title": "Exports",
|
||||
"exports_description": "Generate inline documentation and validation artifacts from the current reviewed state.",
|
||||
"export_documentation_json": "Export documentation (JSON)",
|
||||
"export_documentation_markdown": "Export documentation (Markdown)",
|
||||
"export_validation_json": "Export validation (JSON)",
|
||||
"export_validation_markdown": "Export validation (Markdown)",
|
||||
"preview_title": "Preview status",
|
||||
"preview_status_label": "Status",
|
||||
"preview_compiler_label": "Compiled by",
|
||||
"preview_pending_note": "Compiled preview is not part of this US1 batch yet, but the workspace keeps the state visible when present.",
|
||||
"jump_target_label": "Focused area",
|
||||
"resume_action": "Resume session",
|
||||
"pause_action": "Pause session",
|
||||
"actions": {
|
||||
"import_from_superset": "Import from Superset",
|
||||
"review_documentation": "Review documentation",
|
||||
"apply_semantic_source": "Apply semantic source",
|
||||
"start_clarification": "Start clarification",
|
||||
"answer_next_question": "Answer next question",
|
||||
"approve_mapping": "Approve mapping",
|
||||
"generate_sql_preview": "Generate SQL preview",
|
||||
"complete_required_values": "Complete required values",
|
||||
"launch_dataset": "Launch dataset",
|
||||
"resume_session": "Resume session",
|
||||
"export_outputs": "Export outputs"
|
||||
},
|
||||
"readiness": {
|
||||
"empty": "Empty",
|
||||
"importing": "Importing",
|
||||
"review_ready": "Review ready",
|
||||
"semantic_source_review_needed": "Semantic source review needed",
|
||||
"clarification_needed": "Clarification needed",
|
||||
"clarification_active": "Clarification active",
|
||||
"mapping_review_needed": "Mapping review needed",
|
||||
"compiled_preview_ready": "Compiled preview ready",
|
||||
"partially_ready": "Partially ready",
|
||||
"run_ready": "Run ready",
|
||||
"run_in_progress": "Run in progress",
|
||||
"completed": "Completed",
|
||||
"recovery_required": "Recovery required"
|
||||
},
|
||||
"summary_sources": {
|
||||
"confirmed": "Confirmed",
|
||||
"imported": "Imported",
|
||||
"inferred": "Inferred",
|
||||
"ai_draft": "AI draft",
|
||||
"manual_override": "Manual override"
|
||||
},
|
||||
"confidence": {
|
||||
"confirmed": "Confirmed",
|
||||
"mostly_confirmed": "Mostly confirmed",
|
||||
"mixed": "Mixed",
|
||||
"low_confidence": "Low confidence",
|
||||
"unresolved": "Unresolved"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"management": "Task Management",
|
||||
"run_backup": "Run Backup",
|
||||
|
||||
@@ -584,6 +584,187 @@
|
||||
"task_failed": "Ошибка",
|
||||
"task_waiting": "Ожидание"
|
||||
},
|
||||
"dataset_review": {
|
||||
"source": {
|
||||
"eyebrow": "Источник",
|
||||
"title": "Запуск review датасета",
|
||||
"description": "Вставьте ссылку Superset или укажите ссылку на датасет, чтобы начать возобновляемую review-сессию.",
|
||||
"state_idle": "Ожидание",
|
||||
"state_validating": "Проверка",
|
||||
"state_rejected": "Отклонено",
|
||||
"environment_label": "Окружение",
|
||||
"environment_required": "Перед запуском review выберите окружение Superset.",
|
||||
"superset_link_tab": "Ссылка Superset",
|
||||
"superset_link_tab_hint": "Импортирует контекст dashboard/explore и поэтапно восстанавливает фильтры.",
|
||||
"dataset_selection_tab": "Выбор датасета",
|
||||
"dataset_selection_tab_hint": "Используйте известную ссылку на датасет, если цель уже определена.",
|
||||
"superset_link_label": "URL Superset",
|
||||
"superset_link_placeholder": "https://superset.example.com/superset/dashboard/42/?native_filters=...",
|
||||
"superset_link_hint": "Распознанная ссылка Superset сразу создает сессию, а затем продолжает частичное восстановление в фоне.",
|
||||
"superset_link_required": "Вставьте ссылку Superset, чтобы продолжить.",
|
||||
"superset_link_invalid": "Введите полный URL Superset, начинающийся с http:// или https://.",
|
||||
"superset_link_recovery_note": "Частичное восстановление остается видимым. Отсутствующие фильтры и неразрешенный контекст явно показываются.",
|
||||
"recognized_link_hint": "Ссылка Superset распознана. Восстановление продолжится после создания оболочки сессии.",
|
||||
"dataset_selection_label": "Ссылка на датасет",
|
||||
"dataset_selection_placeholder": "dataset_id:123 или sales.daily_revenue",
|
||||
"dataset_selection_hint": "Используйте id датасета или стабильную ссылку на датасет из выбранного окружения.",
|
||||
"dataset_selection_required": "Введите ссылку на датасет, чтобы продолжить.",
|
||||
"dataset_selection_recovery_note": "Для intake по датасету также явно показываются confidence и неразрешенный контекст.",
|
||||
"dataset_selection_acknowledged": "Ссылка на датасет принята. Загружается возобновляемая review-оболочка.",
|
||||
"submit_superset_link": "Старт по ссылке",
|
||||
"submit_dataset_selection": "Старт по датасету",
|
||||
"submitting": "Запуск review...",
|
||||
"submit_failed": "Не удалось запустить review-сессию датасета."
|
||||
},
|
||||
"findings": {
|
||||
"eyebrow": "Результаты валидации",
|
||||
"title": "Проверьте блокеры, предупреждения и информационные заметки",
|
||||
"description": "Результаты сгруппированы по severity, чтобы самые важные риски оставались видимыми, а частичное восстановление оставалось пригодным к использованию.",
|
||||
"next_action_label": "Рекомендованный следующий шаг",
|
||||
"empty": "Результаты пока не записаны. Сессию можно продолжать с текущим восстановленным контекстом.",
|
||||
"blocking_title": "Блокеры",
|
||||
"blocking_empty": "Открытых блокирующих результатов сейчас нет.",
|
||||
"warning_title": "Предупреждения",
|
||||
"warning_empty": "Сейчас нет warning-результатов, требующих внимания.",
|
||||
"informational_title": "Информационные",
|
||||
"informational_empty": "Информационные результаты сейчас отсутствуют.",
|
||||
"resolution_label": "Статус",
|
||||
"reference_label": "Ссылка",
|
||||
"jump_action": "Открыть связанный раздел",
|
||||
"resolution": {
|
||||
"open": "Открыто",
|
||||
"resolved": "Решено",
|
||||
"approved": "Подтверждено",
|
||||
"skipped": "Пропущено",
|
||||
"deferred": "Отложено",
|
||||
"expert_review": "Экспертная проверка"
|
||||
},
|
||||
"areas": {
|
||||
"source_intake": "Источник",
|
||||
"dataset_profile": "Профиль датасета",
|
||||
"semantic_enrichment": "Семантическое обогащение",
|
||||
"clarification": "Уточнение",
|
||||
"filter_recovery": "Восстановление фильтров",
|
||||
"template_mapping": "Маппинг шаблонов",
|
||||
"compiled_preview": "Скомпилированный preview",
|
||||
"launch": "Запуск",
|
||||
"audit": "Аудит"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"eyebrow": "Оркестрация датасета",
|
||||
"title": "Workspace review датасета",
|
||||
"description": "Возобновляйте предыдущую сессию или просматривайте восстановленный контекст, findings, provenance и exports в одном месте.",
|
||||
"loading": "Загрузка review workspace...",
|
||||
"load_failed": "Не удалось загрузить review-сессию датасета.",
|
||||
"resume_failed": "Не удалось возобновить текущую review-сессию.",
|
||||
"save_failed": "Не удалось обновить review-сессию.",
|
||||
"export_failed": "Не удалось экспортировать выбранный артефакт review.",
|
||||
"empty_state_title": "Сессия не загружена",
|
||||
"state_label": "Workspace",
|
||||
"readiness_label": "Готовность",
|
||||
"source_badge_fallback": "review-сессия",
|
||||
"save_session_action": "Сохранить сессию",
|
||||
"export_summary_action": "Экспортировать summary",
|
||||
"save_summary_action": "Сохранить summary",
|
||||
"summary_edit_saved": "Черновик summary обновлен для текущего review workspace.",
|
||||
"import_progress_eyebrow": "Прогресс импорта",
|
||||
"import_progress_title": "Контекст датасета восстанавливается поэтапно",
|
||||
"import_progress_body": "Workspace последовательно показывает этапы распознавания источника, фильтров, переменных и семантических кандидатов.",
|
||||
"import_milestones": {
|
||||
"recognized": "Датасет распознан",
|
||||
"filters": "Сохраненные native filters восстановлены",
|
||||
"variables": "Переменные шаблона датасета обнаружены",
|
||||
"semantic_sources": "Найдены ближайшие семантические источники",
|
||||
"summary": "Подготовлено предварительное summary"
|
||||
},
|
||||
"source_session_title": "Источник и сессия",
|
||||
"source_label": "Источник датасета",
|
||||
"import_status_title": "Статус импорта Superset",
|
||||
"recent_actions_title": "Таймлайн последних действий",
|
||||
"timeline": {
|
||||
"source": "Источник принят",
|
||||
"status": "Состояние сессии обновлено",
|
||||
"filters": "Фильтры восстановлены",
|
||||
"findings": "Findings записаны",
|
||||
"exports": "Последний экспорт создан"
|
||||
},
|
||||
"next_action_label": "Следующее действие",
|
||||
"next_action_card_eyebrow": "Основное действие",
|
||||
"next_action_card_body": "Основное действие всегда соответствует самому ценному следующему шагу для текущего состояния сессии.",
|
||||
"session_label": "Сводка сессии",
|
||||
"session_id_label": "ID сессии",
|
||||
"summary_source_label": "Источник summary",
|
||||
"confidence_label": "Уверенность",
|
||||
"business_summary_title": "Бизнес-summary",
|
||||
"summary_missing": "Читаемое бизнес-summary пока недоступно.",
|
||||
"partial_recovery_badge": "Частичное восстановление",
|
||||
"phase_label": "Фаза",
|
||||
"status_label": "Статус",
|
||||
"active_task_label": "Активная задача",
|
||||
"recovery_title": "Восстановленные фильтры и provenance",
|
||||
"recovery_empty": "Импортированные фильтры пока не восстановлены.",
|
||||
"provenance_label": "Provenance",
|
||||
"recovered_value_label": "Восстановленное значение",
|
||||
"health_title": "Обзор findings",
|
||||
"open_findings_label": "Открытые findings",
|
||||
"exports_title": "Экспорт",
|
||||
"exports_description": "Генерируйте inline-артефакты документации и валидации из текущего review-состояния.",
|
||||
"export_documentation_json": "Экспорт документации (JSON)",
|
||||
"export_documentation_markdown": "Экспорт документации (Markdown)",
|
||||
"export_validation_json": "Экспорт валидации (JSON)",
|
||||
"export_validation_markdown": "Экспорт валидации (Markdown)",
|
||||
"preview_title": "Статус preview",
|
||||
"preview_status_label": "Статус",
|
||||
"preview_compiler_label": "Скомпилировано",
|
||||
"preview_pending_note": "Скомпилированный preview не входит в этот пакет US1, но workspace сохраняет его видимым, если он уже существует.",
|
||||
"jump_target_label": "Выбранная зона",
|
||||
"resume_action": "Возобновить сессию",
|
||||
"pause_action": "Поставить сессию на паузу",
|
||||
"actions": {
|
||||
"import_from_superset": "Импорт из Superset",
|
||||
"review_documentation": "Проверить документацию",
|
||||
"apply_semantic_source": "Применить семантический источник",
|
||||
"start_clarification": "Начать уточнение",
|
||||
"answer_next_question": "Ответить на следующий вопрос",
|
||||
"approve_mapping": "Подтвердить маппинг",
|
||||
"generate_sql_preview": "Сгенерировать SQL preview",
|
||||
"complete_required_values": "Заполнить обязательные значения",
|
||||
"launch_dataset": "Запустить датасет",
|
||||
"resume_session": "Возобновить сессию",
|
||||
"export_outputs": "Экспортировать результаты"
|
||||
},
|
||||
"readiness": {
|
||||
"empty": "Пусто",
|
||||
"importing": "Импорт",
|
||||
"review_ready": "Готово к review",
|
||||
"semantic_source_review_needed": "Нужен review источника семантики",
|
||||
"clarification_needed": "Нужно уточнение",
|
||||
"clarification_active": "Уточнение активно",
|
||||
"mapping_review_needed": "Нужен review маппинга",
|
||||
"compiled_preview_ready": "Скомпилированный preview готов",
|
||||
"partially_ready": "Частично готово",
|
||||
"run_ready": "Готово к запуску",
|
||||
"run_in_progress": "Запуск выполняется",
|
||||
"completed": "Завершено",
|
||||
"recovery_required": "Требуется восстановление"
|
||||
},
|
||||
"summary_sources": {
|
||||
"confirmed": "Подтверждено",
|
||||
"imported": "Импортировано",
|
||||
"inferred": "Выведено",
|
||||
"ai_draft": "Черновик AI",
|
||||
"manual_override": "Ручное переопределение"
|
||||
},
|
||||
"confidence": {
|
||||
"confirmed": "Подтверждено",
|
||||
"mostly_confirmed": "Почти подтверждено",
|
||||
"mixed": "Смешанное",
|
||||
"low_confidence": "Низкая уверенность",
|
||||
"unresolved": "Не разрешено"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"management": "Управление задачами",
|
||||
"run_backup": "Запустить бэкап",
|
||||
|
||||
16
frontend/src/lib/stores/__tests__/mocks/state.js
Normal file
16
frontend/src/lib/stores/__tests__/mocks/state.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// [DEF:state_mock:Module]
|
||||
// @COMPLEXITY: 2
|
||||
// @PURPOSE: Mock for $app/state in vitest route/component tests.
|
||||
// @LAYER: UI Tests
|
||||
|
||||
export const page = {
|
||||
params: {},
|
||||
route: { id: "test" },
|
||||
url: new URL("http://localhost"),
|
||||
status: 200,
|
||||
error: null,
|
||||
data: {},
|
||||
form: null,
|
||||
};
|
||||
|
||||
// [/DEF:state_mock:Module]
|
||||
@@ -1,9 +1,10 @@
|
||||
// [DEF:frontend.src.lib.stores.__tests__.test_datasetReviewSession:Module]
|
||||
// [DEF:DatasetReviewSessionStoreTests:Module]
|
||||
// @COMPLEXITY: 3
|
||||
// @SEMANTICS: dataset-review, store, session, tests
|
||||
// @PURPOSE: Unit tests for dataset review session store
|
||||
// @PURPOSE: Unit tests for dataset review session store.
|
||||
// @LAYER: UI
|
||||
// @RELATION: VERIFIES -> [datasetReviewSession:Store]
|
||||
// @UX_STATE: Idle -> Store helpers are exercised without asynchronous UI transitions.
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
@@ -109,4 +110,4 @@ describe('datasetReviewSession store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// [/DEF:frontend.src.lib.stores.__tests__.test_datasetReviewSession:Module]
|
||||
// [/DEF:DatasetReviewSessionStoreTests:Module]
|
||||
@@ -1,8 +1,12 @@
|
||||
// [DEF:datasetReviewSession:Store]
|
||||
// @COMPLEXITY: 4
|
||||
// @PURPOSE: Manage active dataset review session state, including loading, updates, and navigation guards.
|
||||
// @PURPOSE: Manage active dataset review session state, including loading, local edits, error capture, and reset semantics for the active review workspace.
|
||||
// @LAYER: UI
|
||||
// @RELATION: DEPENDS_ON -> api_module (requestApi/fetchApi)
|
||||
// @RELATION: DEPENDS_ON -> [api_module]
|
||||
// @PRE: Consumers provide session-shaped payloads when setting or patching state and initialize the store before reading derived session fields.
|
||||
// @POST: Store transitions keep session, loading, error, and dirty flags internally consistent after each helper call.
|
||||
// @SIDE_EFFECT: Mutates the writable dataset review session store in frontend memory.
|
||||
// @DATA_CONTRACT: Input[SessionDetail | Partial<SessionDetail> | boolean | string | null] -> Output[DatasetReviewSessionStoreState]
|
||||
//
|
||||
// @UX_STATE: Loading -> Session detail is being fetched.
|
||||
// @UX_STATE: Ready -> Session detail is available for UI binding.
|
||||
|
||||
Reference in New Issue
Block a user