332 lines
10 KiB
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] --> |