Files
ss-tools/frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte

332 lines
10 KiB
Svelte

<!-- [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] -->