feat(ui): surface review readiness and blockers
Add readiness hints, blocker summaries, and progress cues across the dataset review workspace to clarify partial recovery and launch gating states. Highlight read-only preview snapshots, pending semantic review, clarification queue status, and unresolved validation blockers to guide the next recommended action. Document known limitations in backend test doubles so permission, task dispatch, and uncovered persistence branches are explicit.
This commit is contained in:
@@ -135,6 +135,35 @@
|
||||
return text.slice(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
function getReadinessTone(readinessState) {
|
||||
if (readinessState === "run_ready") return "bg-emerald-100 text-emerald-700";
|
||||
if (
|
||||
readinessState === "partially_ready" ||
|
||||
readinessState === "recovery_required" ||
|
||||
readinessState === "compiled_preview_ready"
|
||||
) {
|
||||
return "bg-amber-100 text-amber-800";
|
||||
}
|
||||
if (readinessState === "importing") return "bg-blue-100 text-blue-700";
|
||||
return "bg-slate-100 text-slate-700";
|
||||
}
|
||||
|
||||
function getReadinessHint(readinessState) {
|
||||
if (readinessState === "recovery_required") {
|
||||
return "Контекст восстановлен частично: перед запуском проверьте фильтры, маппинг и превью SQL.";
|
||||
}
|
||||
if (readinessState === "partially_ready") {
|
||||
return "Сессия продолжает быть полезной, но запуск остаётся заблокирован до обновления превью и закрытия блокеров.";
|
||||
}
|
||||
if (readinessState === "compiled_preview_ready") {
|
||||
return "Превью собрано. Проверьте approvals и launch-блокеры перед запуском.";
|
||||
}
|
||||
if (readinessState === "run_ready") {
|
||||
return "Сессия готова к запуску после финальной проверки.";
|
||||
}
|
||||
return "Откройте сессию, чтобы увидеть следующий рекомендуемый шаг.";
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
isBootstrapping = true;
|
||||
try {
|
||||
@@ -351,7 +380,11 @@
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
<span class="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700">
|
||||
<span
|
||||
class={`rounded-full px-2.5 py-1 text-xs font-medium ${getReadinessTone(
|
||||
session.readiness_state,
|
||||
)}`}
|
||||
>
|
||||
{getSessionMetaValue(session, "readiness_state", $t.common?.unknown || "unknown")}
|
||||
</span>
|
||||
<span class="rounded-full bg-purple-50 px-2.5 py-1 text-xs font-medium text-purple-700">
|
||||
@@ -359,6 +392,10 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-xs text-slate-600">
|
||||
{getReadinessHint(session.readiness_state)}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg bg-slate-50 px-3 py-2">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">
|
||||
@@ -504,4 +541,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:DatasetReviewWorkspaceEntry:Page] -->
|
||||
<!-- [/DEF:DatasetReviewWorkspaceEntry:Page] -->
|
||||
|
||||
@@ -86,6 +86,20 @@
|
||||
const latestRunContext = $derived(
|
||||
session?.run_contexts?.length ? session.run_contexts[session.run_contexts.length - 1] : null,
|
||||
);
|
||||
const clarificationSession = $derived(clarificationState?.clarification_session || null);
|
||||
const clarificationCurrentQuestion = $derived(clarificationState?.current_question || null);
|
||||
const clarificationRemainingCount = $derived(
|
||||
Number(clarificationSession?.remaining_count || 0),
|
||||
);
|
||||
const clarificationResolvedCount = $derived(
|
||||
Number(clarificationSession?.resolved_count || 0),
|
||||
);
|
||||
const clarificationTotalCount = $derived(
|
||||
clarificationRemainingCount + clarificationResolvedCount,
|
||||
);
|
||||
const previewGateState = $derived(
|
||||
String(previewUiState || latestPreview?.preview_status || "missing"),
|
||||
);
|
||||
const exportDisabled = $derived(!session || isExporting);
|
||||
const saveDisabled = $derived(!session || isSubmitting);
|
||||
const launchDisabled = $derived(!session || isSubmitting || isExporting);
|
||||
@@ -117,6 +131,28 @@
|
||||
);
|
||||
const importMilestones = $derived(buildImportMilestones());
|
||||
const recentActions = $derived(buildRecentActions());
|
||||
const unresolvedBlockingFindingsCount = $derived(
|
||||
findings.filter(
|
||||
(finding) =>
|
||||
finding.severity === "blocking" &&
|
||||
finding.resolution_state !== "resolved" &&
|
||||
finding.resolution_state !== "approved",
|
||||
).length,
|
||||
);
|
||||
const pendingApprovalMappingsCount = $derived(
|
||||
executionMappings.filter(
|
||||
(mapping) =>
|
||||
mapping.requires_explicit_approval &&
|
||||
mapping.approval_state !== "approved",
|
||||
).length,
|
||||
);
|
||||
const workspaceLaunchBlockers = $derived(buildWorkspaceLaunchBlockers());
|
||||
|
||||
function normalizeJumpTarget(target) {
|
||||
if (target === "preview") return "sql-preview";
|
||||
if (target === "mappings") return "mapping";
|
||||
return target || "";
|
||||
}
|
||||
|
||||
function getWorkspaceStateLabel(state) {
|
||||
return $t.dataset_review?.workspace?.state?.[state] || state;
|
||||
@@ -215,6 +251,27 @@
|
||||
return actions;
|
||||
}
|
||||
|
||||
function buildWorkspaceLaunchBlockers() {
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const blockers = [];
|
||||
if (unresolvedBlockingFindingsCount > 0) {
|
||||
blockers.push(`Нерешённых blocking findings: ${unresolvedBlockingFindingsCount}`);
|
||||
}
|
||||
if (pendingApprovalMappingsCount > 0) {
|
||||
blockers.push(`Требуют approval в маппинге: ${pendingApprovalMappingsCount}`);
|
||||
}
|
||||
if (previewGateState !== "ready") {
|
||||
blockers.push(`Состояние SQL preview: ${previewGateState}`);
|
||||
}
|
||||
if (String(session.readiness_state || "") !== "run_ready") {
|
||||
blockers.push(`Readiness: ${session.readiness_state || "unknown"}`);
|
||||
}
|
||||
return blockers;
|
||||
}
|
||||
|
||||
function buildSessionUrl(sessionId) {
|
||||
return `/datasets/review/${encodeURIComponent(String(sessionId))}`;
|
||||
}
|
||||
@@ -365,7 +422,7 @@
|
||||
}
|
||||
|
||||
function handleFindingJump(event) {
|
||||
jumpTarget = event?.target || "summary";
|
||||
jumpTarget = normalizeJumpTarget(event?.target || "summary");
|
||||
}
|
||||
|
||||
function startSummaryEdit() {
|
||||
@@ -497,7 +554,7 @@
|
||||
}
|
||||
|
||||
function handleSectionJump(event) {
|
||||
jumpTarget = event?.target || "";
|
||||
jumpTarget = normalizeJumpTarget(event?.target || "");
|
||||
}
|
||||
|
||||
function primaryActionHandler() {
|
||||
@@ -530,6 +587,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimaryActionCtaLabel() {
|
||||
if (!session) {
|
||||
return $t.dataset_review?.workspace?.next_action_label;
|
||||
}
|
||||
if (session.recommended_action === "resume_session") {
|
||||
return $t.dataset_review?.workspace?.resume_action;
|
||||
}
|
||||
if (session.recommended_action === "generate_sql_preview") {
|
||||
return $t.dataset_review?.preview?.generate_action;
|
||||
}
|
||||
if (session.recommended_action === "approve_mapping") {
|
||||
return $t.dataset_review?.mapping?.approve_all_action;
|
||||
}
|
||||
if (session.recommended_action === "launch_dataset") {
|
||||
return $t.dataset_review?.launch?.launch_action;
|
||||
}
|
||||
return $t.dataset_review?.workspace?.pause_action;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const target = normalizeJumpTarget(jumpTarget);
|
||||
if (!target || typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
document.getElementById(target)?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
});
|
||||
|
||||
bootstrapWorkspace();
|
||||
</script>
|
||||
|
||||
@@ -583,6 +670,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if session}
|
||||
<section class="rounded-2xl border border-slate-200 bg-white px-4 py-4 shadow-sm">
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Фаза</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{session.current_phase}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Preview gate</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{previewGateState}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Clarification queue</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">
|
||||
{clarificationRemainingCount} remaining / {clarificationTotalCount}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-3">
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Launch blockers</div>
|
||||
<div class="mt-1 text-sm font-semibold text-slate-900">{workspaceLaunchBlockers.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if session.readiness_state === "recovery_required" || session.readiness_state === "partially_ready"}
|
||||
<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-800">
|
||||
Сессия в частично-восстановленном режиме: материалы можно просматривать и уточнять, но запуск будет заблокирован до закрытия блокеров и актуального SQL preview.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if previewGateState === "stale" || previewGateState === "failed" || previewGateState === "pending"}
|
||||
<div class="mt-3 rounded-xl border border-blue-200 bg-blue-50 px-3 py-3 text-sm text-blue-800">
|
||||
Preview сейчас в состоянии <span class="font-semibold">{previewGateState}</span>. Это read-only/промежуточный снимок и не финальное основание для запуска.
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if isBootstrapping}
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-500 shadow-sm">
|
||||
{$t.dataset_review?.workspace?.loading}
|
||||
@@ -687,6 +811,16 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if session.readiness_state === "recovery_required"}
|
||||
<p class="mt-3 text-xs text-amber-700">
|
||||
Режим partial recovery: часть контекста импортирована, часть требует ручной проверки.
|
||||
</p>
|
||||
{:else if session.readiness_state === "partially_ready"}
|
||||
<p class="mt-3 text-xs text-amber-700">
|
||||
Снимок промежуточный: продолжайте review, затем пересоберите preview перед launch.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<ol class="mt-4 space-y-2">
|
||||
{#each importMilestones as milestone}
|
||||
<li class="flex items-center gap-2 text-sm text-slate-700">
|
||||
@@ -931,6 +1065,19 @@
|
||||
/>
|
||||
|
||||
{#if clarificationState || session.clarification_sessions?.length}
|
||||
<div class="rounded-xl border border-violet-200 bg-violet-50 px-4 py-3 text-sm text-violet-900">
|
||||
<div class="font-medium">Очередь уточнений</div>
|
||||
{#if clarificationCurrentQuestion}
|
||||
<p class="mt-1 text-xs text-violet-700">
|
||||
Следующий вопрос уже готов. Осталось: {clarificationRemainingCount}.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="mt-1 text-xs text-violet-700">
|
||||
Активного вопроса нет. Можно продолжить через Resume clarification.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ClarificationDialog
|
||||
sessionId={session.session_id}
|
||||
clarificationState={clarificationState}
|
||||
@@ -976,9 +1123,7 @@
|
||||
class="mt-4 inline-flex w-full items-center justify-center rounded-xl bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700"
|
||||
onclick={primaryActionHandler}
|
||||
>
|
||||
{session.recommended_action === "resume_session"
|
||||
? $t.dataset_review?.workspace?.resume_action
|
||||
: $t.dataset_review?.workspace?.pause_action}
|
||||
{getPrimaryActionCtaLabel()}
|
||||
</button>
|
||||
|
||||
{#if jumpTarget}
|
||||
@@ -986,6 +1131,19 @@
|
||||
{$t.dataset_review?.workspace?.jump_target_label}: {jumpTarget}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if workspaceLaunchBlockers.length > 0}
|
||||
<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-amber-700">
|
||||
Launch blockers snapshot
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1 text-xs text-amber-800">
|
||||
{#each workspaceLaunchBlockers.slice(0, 4) as blocker}
|
||||
<li>• {blocker}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user