Migrate Datasources interface patterns
This commit is contained in:
@@ -20,9 +20,11 @@ import {
|
||||
Upload
|
||||
} from "lucide-react";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
IconButton,
|
||||
@@ -31,6 +33,8 @@ import {
|
||||
StatusBadge,
|
||||
hasScope,
|
||||
isApiError,
|
||||
useUnsavedChanges,
|
||||
useUnsavedDraftGuard,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
@@ -54,6 +58,12 @@ import {
|
||||
type DatasourcePreview,
|
||||
type DatasourceStage
|
||||
} from "../../api/datasources";
|
||||
import {
|
||||
DATASOURCE_FIELDS_DOCUMENTATION,
|
||||
DATASOURCE_GOVERNANCE_DOCUMENTATION,
|
||||
DATASOURCES_DOCUMENTATION,
|
||||
DATASOURCES_I18N
|
||||
} from "./interfacePatterns";
|
||||
|
||||
type CatalogueView = "catalogue" | "staging" | "origins";
|
||||
type AddKind = "upload" | "origin";
|
||||
@@ -86,7 +96,9 @@ export default function DatasourcesPage({
|
||||
const [freezeOpen, setFreezeOpen] = useState(false);
|
||||
const [freezeLabel, setFreezeLabel] = useState("");
|
||||
const [retireOpen, setRetireOpen] = useState(false);
|
||||
const [promoteOpen, setPromoteOpen] = useState(false);
|
||||
const [governanceOpen, setGovernanceOpen] = useState(false);
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
|
||||
const canManage = hasScope(auth, "datasources:source:write")
|
||||
|| hasScope(auth, "datasources:source:admin");
|
||||
@@ -211,8 +223,8 @@ export default function DatasourcesPage({
|
||||
}
|
||||
};
|
||||
|
||||
const freezeSelected = async () => {
|
||||
if (!selectedDatasource) return;
|
||||
const freezeSelected = async (): Promise<boolean> => {
|
||||
if (!selectedDatasource) return false;
|
||||
setWorking(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -225,8 +237,10 @@ export default function DatasourcesPage({
|
||||
setFreezeOpen(false);
|
||||
setFreezeLabel("");
|
||||
await reload(selectedDatasource.ref);
|
||||
return true;
|
||||
} catch (operationError) {
|
||||
setError(apiErrorMessage(operationError));
|
||||
return false;
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
@@ -248,6 +262,23 @@ export default function DatasourcesPage({
|
||||
}
|
||||
};
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: Boolean(freezeOpen && freezeLabel.trim()),
|
||||
onSave: freezeSelected,
|
||||
onDiscard: () => {
|
||||
setFreezeOpen(false);
|
||||
setFreezeLabel("");
|
||||
},
|
||||
title: "i18n:govoplan-datasources.unsaved_freeze_title",
|
||||
message: "i18n:govoplan-datasources.unsaved_freeze_message"
|
||||
});
|
||||
|
||||
const closeFreeze = () => {
|
||||
if (working) return;
|
||||
if (freezeLabel.trim()) requestDiscard(() => setFreezeOpen(false));
|
||||
else setFreezeOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="datasources-page">
|
||||
<div className="datasources-shell">
|
||||
@@ -261,6 +292,7 @@ export default function DatasourcesPage({
|
||||
variant="ghost"
|
||||
onClick={() => void reload(selectedDatasourceRef)}
|
||||
disabled={loading || working}
|
||||
disabledReason={loading ? DATASOURCES_I18N.loading : working ? DATASOURCES_I18N.working : undefined}
|
||||
/>
|
||||
<IconButton
|
||||
label="Add datasource or stage"
|
||||
@@ -268,6 +300,7 @@ export default function DatasourcesPage({
|
||||
variant="primary"
|
||||
onClick={() => setAddOpen(true)}
|
||||
disabled={!canStage && !canManage}
|
||||
disabledReason={!canStage && !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -384,8 +417,9 @@ export default function DatasourcesPage({
|
||||
</span>
|
||||
</span>
|
||||
<span className="datasources-toolbar-actions">
|
||||
<DocumentationHelpLink reference={DATASOURCES_DOCUMENTATION} />
|
||||
{view === "catalogue" && selectedDatasource?.mode === "cached" ? (
|
||||
<Button onClick={() => void refreshSelected()} disabled={!canManage || working}>
|
||||
<Button onClick={() => void refreshSelected()} disabled={!canManage || working} disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}>
|
||||
<Download size={16} /> Refresh
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -396,8 +430,9 @@ export default function DatasourcesPage({
|
||||
icon={<Pencil size={16} />}
|
||||
onClick={() => setGovernanceOpen(true)}
|
||||
disabled={!canManage || working}
|
||||
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||
/>
|
||||
<Button onClick={() => setFreezeOpen(true)} disabled={!canManage || working}>
|
||||
<Button onClick={() => setFreezeOpen(true)} disabled={!canManage || working} disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}>
|
||||
<Snowflake size={16} /> Freeze
|
||||
</Button>
|
||||
<IconButton
|
||||
@@ -406,14 +441,16 @@ export default function DatasourcesPage({
|
||||
variant="danger"
|
||||
onClick={() => setRetireOpen(true)}
|
||||
disabled={!canManage || working}
|
||||
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{view === "staging" && selectedStage?.state === "ready" ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void promoteStage()}
|
||||
onClick={() => setPromoteOpen(true)}
|
||||
disabled={!canStage || working}
|
||||
disabledReason={working ? DATASOURCES_I18N.working : !canStage ? DATASOURCES_I18N.stageReason : undefined}
|
||||
>
|
||||
<Upload size={16} /> Promote
|
||||
</Button>
|
||||
@@ -423,6 +460,7 @@ export default function DatasourcesPage({
|
||||
variant="primary"
|
||||
onClick={() => setAddOpen(true)}
|
||||
disabled={!canManage || working}
|
||||
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||
>
|
||||
<Plus size={16} /> Register
|
||||
</Button>
|
||||
@@ -441,6 +479,18 @@ export default function DatasourcesPage({
|
||||
{success}
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
{!canManage && !canStage ? <ActionBlockerHint
|
||||
tone="info"
|
||||
reason={{
|
||||
summary: "Datasources are read-only",
|
||||
details: DATASOURCES_I18N.manageReason,
|
||||
requiredAction: DATASOURCES_I18N.permissionAction,
|
||||
actor: DATASOURCES_I18N.permissionActor,
|
||||
target: DATASOURCES_I18N.permissionDestination
|
||||
}}
|
||||
labels={{ requiredAction: DATASOURCES_I18N.requiredAction, actor: DATASOURCES_I18N.actor, target: DATASOURCES_I18N.destination }}
|
||||
documentation={DATASOURCES_DOCUMENTATION}
|
||||
/> : null}
|
||||
</div>
|
||||
|
||||
<div className="datasources-content">
|
||||
@@ -510,12 +560,10 @@ export default function DatasourcesPage({
|
||||
<Dialog
|
||||
open={freezeOpen}
|
||||
title="Freeze datasource state"
|
||||
onClose={() => {
|
||||
if (!working) setFreezeOpen(false);
|
||||
}}
|
||||
onClose={closeFreeze}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={() => setFreezeOpen(false)} disabled={working}>Cancel</Button>
|
||||
<Button onClick={closeFreeze} disabled={working} disabledReason={working ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||
<Button variant="primary" onClick={() => void freezeSelected()} disabled={working}>
|
||||
<Snowflake size={16} /> Freeze
|
||||
</Button>
|
||||
@@ -525,7 +573,7 @@ export default function DatasourcesPage({
|
||||
<p className="datasources-dialog-copy">
|
||||
Create an immutable, addressable state for reproducible runs and evidence.
|
||||
</p>
|
||||
<FormField label="Label">
|
||||
<FormField label="Label" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<input
|
||||
value={freezeLabel}
|
||||
onChange={(event) => setFreezeLabel(event.target.value)}
|
||||
@@ -533,6 +581,18 @@ export default function DatasourcesPage({
|
||||
/>
|
||||
</FormField>
|
||||
</Dialog>
|
||||
<ConfirmDialog
|
||||
open={promoteOpen}
|
||||
title="i18n:govoplan-datasources.promote_title"
|
||||
message="i18n:govoplan-datasources.promote_message"
|
||||
confirmLabel="Promote"
|
||||
busy={working}
|
||||
onCancel={() => setPromoteOpen(false)}
|
||||
onConfirm={() => {
|
||||
setPromoteOpen(false);
|
||||
void promoteStage();
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
open={retireOpen}
|
||||
title="Retire datasource"
|
||||
@@ -742,19 +802,27 @@ function GovernanceDialog({
|
||||
const [draft, setDraft] = useState<DatasourceGovernance | null>(null);
|
||||
const [freshness, setFreshness] = useState("{}");
|
||||
const [quality, setQuality] = useState("{}");
|
||||
const [baselineKey, setBaselineKey] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !datasource) return;
|
||||
setDraft(structuredClone(datasource.governance));
|
||||
setFreshness(JSON.stringify(datasource.governance.freshness_policy, null, 2));
|
||||
setQuality(JSON.stringify(datasource.governance.quality_policy, null, 2));
|
||||
const nextDraft = structuredClone(datasource.governance);
|
||||
const nextFreshness = JSON.stringify(datasource.governance.freshness_policy, null, 2);
|
||||
const nextQuality = JSON.stringify(datasource.governance.quality_policy, null, 2);
|
||||
setDraft(nextDraft);
|
||||
setFreshness(nextFreshness);
|
||||
setQuality(nextQuality);
|
||||
setBaselineKey(JSON.stringify({ draft: nextDraft, freshness: nextFreshness, quality: nextQuality }));
|
||||
setError("");
|
||||
}, [datasource, open]);
|
||||
|
||||
const save = async () => {
|
||||
if (!datasource || !draft) return;
|
||||
const dirty = Boolean(open && draft && JSON.stringify({ draft, freshness, quality }) !== baselineKey);
|
||||
|
||||
const save = async (): Promise<boolean> => {
|
||||
if (!datasource || !draft) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -764,13 +832,29 @@ function GovernanceDialog({
|
||||
quality_policy: parseObject(quality, "Quality policy")
|
||||
});
|
||||
await onSaved(updated);
|
||||
return true;
|
||||
} catch (saveError) {
|
||||
setError(apiErrorMessage(saveError));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: onClose,
|
||||
title: "i18n:govoplan-datasources.unsaved_governance_title",
|
||||
message: "i18n:govoplan-datasources.unsaved_governance_message"
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (busy) return;
|
||||
if (dirty) requestDiscard(onClose);
|
||||
else onClose();
|
||||
};
|
||||
|
||||
const setValue = <K extends keyof DatasourceGovernance>(
|
||||
key: K,
|
||||
value: DatasourceGovernance[K]
|
||||
@@ -781,11 +865,11 @@ function GovernanceDialog({
|
||||
open={open}
|
||||
title="Datasource governance"
|
||||
className="datasources-governance-dialog"
|
||||
onClose={() => { if (!busy) onClose(); }}
|
||||
onClose={close}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button variant="primary" onClick={() => void save()} disabled={!draft || busy}>
|
||||
<Button onClick={close} disabled={busy} disabledReason={busy ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||
<Button variant="primary" onClick={() => void save()} disabled={!draft || busy} disabledReason={busy ? DATASOURCES_I18N.working : !draft ? DATASOURCES_I18N.incomplete : undefined}>
|
||||
Save governance
|
||||
</Button>
|
||||
</>
|
||||
@@ -795,7 +879,7 @@ function GovernanceDialog({
|
||||
{draft ? (
|
||||
<div className="datasources-dialog-fields">
|
||||
<div className="datasources-dialog-grid">
|
||||
<FormField label="Authority mode">
|
||||
<FormField label="Authority mode" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<select
|
||||
value={draft.authority_mode}
|
||||
onChange={(event) => setValue("authority_mode", event.target.value as DatasourceGovernance["authority_mode"])}
|
||||
@@ -810,13 +894,13 @@ function GovernanceDialog({
|
||||
].map((value) => <option key={value} value={value}>{readableToken(value)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Authoritative source">
|
||||
<FormField label="Authoritative source" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<input value={draft.authoritative_source_ref ?? ""} onChange={(event) => setValue("authoritative_source_ref", event.target.value || null)} />
|
||||
</FormField>
|
||||
<FormField label="Classification">
|
||||
<FormField label="Classification" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<input value={draft.classification} onChange={(event) => setValue("classification", event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Publication state">
|
||||
<FormField label="Publication state" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<input value={draft.publication_state} onChange={(event) => setValue("publication_state", event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Owner reference">
|
||||
@@ -847,7 +931,7 @@ function GovernanceDialog({
|
||||
<input value={draft.correction_procedure_ref ?? ""} onChange={(event) => setValue("correction_procedure_ref", event.target.value || null)} />
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="Semantic definition">
|
||||
<FormField label="Semantic definition" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<textarea value={draft.semantic_definition ?? ""} onChange={(event) => setValue("semantic_definition", event.target.value || null)} />
|
||||
</FormField>
|
||||
<div className="datasources-dialog-grid">
|
||||
@@ -860,10 +944,10 @@ function GovernanceDialog({
|
||||
<GovernanceListField label="Known limits" values={draft.known_limits} onChange={(values) => setValue("known_limits", values)} />
|
||||
</div>
|
||||
<div className="datasources-dialog-grid">
|
||||
<FormField label="Freshness policy (JSON)">
|
||||
<FormField label="Freshness policy (JSON)" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<textarea value={freshness} onChange={(event) => setFreshness(event.target.value)} spellCheck={false} />
|
||||
</FormField>
|
||||
<FormField label="Quality policy (JSON)">
|
||||
<FormField label="Quality policy (JSON)" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||
<textarea value={quality} onChange={(event) => setQuality(event.target.value)} spellCheck={false} />
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -931,22 +1015,54 @@ function AddDatasourceDialog({
|
||||
const [rowsText, setRowsText] = useState('[\n { "id": 1 }\n]');
|
||||
const [csvText, setCsvText] = useState("");
|
||||
const [delimiter, setDelimiter] = useState(";");
|
||||
const [baselineKey, setBaselineKey] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setKind(initialKind);
|
||||
setFormat("csv");
|
||||
setOriginRef(initialOrigin?.ref ?? "");
|
||||
setName(initialOrigin?.name ?? "");
|
||||
setSourceName(initialOrigin?.source_name ?? "");
|
||||
setDescription(initialOrigin?.description ?? "");
|
||||
setMode(initialKind === "origin" ? "live" : "static");
|
||||
setTargetRef("");
|
||||
setRowsText('[\n { "id": 1 }\n]');
|
||||
setCsvText("");
|
||||
setDelimiter(";");
|
||||
setBaselineKey(addDatasourceDraftKey({
|
||||
kind: initialKind,
|
||||
format: "csv",
|
||||
mode: initialKind === "origin" ? "live" : "static",
|
||||
originRef: initialOrigin?.ref ?? "",
|
||||
targetRef: "",
|
||||
name: initialOrigin?.name ?? "",
|
||||
sourceName: initialOrigin?.source_name ?? "",
|
||||
description: initialOrigin?.description ?? "",
|
||||
rowsText: '[\n { "id": 1 }\n]',
|
||||
csvText: "",
|
||||
delimiter: ";"
|
||||
}));
|
||||
setError("");
|
||||
}, [initialKind, initialOrigin, open]);
|
||||
|
||||
const selectedOrigin = origins.find((item) => item.ref === originRef) ?? null;
|
||||
const dirty = Boolean(open && addDatasourceDraftKey({
|
||||
kind,
|
||||
format,
|
||||
mode,
|
||||
originRef,
|
||||
targetRef,
|
||||
name,
|
||||
sourceName,
|
||||
description,
|
||||
rowsText,
|
||||
csvText,
|
||||
delimiter
|
||||
}) !== baselineKey);
|
||||
|
||||
const chooseOrigin = (ref: string) => {
|
||||
const origin = origins.find((item) => item.ref === ref);
|
||||
@@ -968,7 +1084,7 @@ function AddDatasourceDialog({
|
||||
setMode(target.mode === "cached" ? "cached" : "static");
|
||||
};
|
||||
|
||||
const create = async () => {
|
||||
const create = async (): Promise<boolean> => {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -1006,13 +1122,29 @@ function AddDatasourceDialog({
|
||||
});
|
||||
await onCreated(stage);
|
||||
}
|
||||
return true;
|
||||
} catch (createError) {
|
||||
setError(apiErrorMessage(createError));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: create,
|
||||
onDiscard: onClose,
|
||||
title: "i18n:govoplan-datasources.unsaved_add_title",
|
||||
message: "i18n:govoplan-datasources.unsaved_add_message"
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (busy) return;
|
||||
if (dirty) requestDiscard(onClose);
|
||||
else onClose();
|
||||
};
|
||||
|
||||
const loadFile = async (file: File | undefined) => {
|
||||
if (!file) return;
|
||||
try {
|
||||
@@ -1034,12 +1166,10 @@ function AddDatasourceDialog({
|
||||
open={open}
|
||||
title="Add datasource"
|
||||
className="datasources-add-dialog"
|
||||
onClose={() => {
|
||||
if (!busy) onClose();
|
||||
}}
|
||||
onClose={close}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
||||
<Button onClick={close} disabled={busy} disabledReason={busy ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void create()}
|
||||
@@ -1050,6 +1180,7 @@ function AddDatasourceDialog({
|
||||
|| (kind === "upload" && !canStage)
|
||||
|| (kind === "origin" && (!canManage || !originRef))
|
||||
}
|
||||
disabledReason={busy ? DATASOURCES_I18N.working : !name.trim() || !sourceName.trim() ? DATASOURCES_I18N.incomplete : kind === "upload" && !canStage ? DATASOURCES_I18N.stageReason : kind === "origin" && !canManage ? DATASOURCES_I18N.manageReason : kind === "origin" && !originRef ? DATASOURCES_I18N.incomplete : undefined}
|
||||
>
|
||||
{kind === "upload" ? <Upload size={16} /> : <Database size={16} />}
|
||||
{kind === "upload" ? "Create stage" : "Register"}
|
||||
@@ -1081,7 +1212,7 @@ function AddDatasourceDialog({
|
||||
}}
|
||||
/>
|
||||
{kind === "origin" ? (
|
||||
<FormField label="Connector origin">
|
||||
<FormField label="Connector origin" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<select value={originRef} onChange={(event) => chooseOrigin(event.target.value)}>
|
||||
<option value="">Choose an origin</option>
|
||||
{origins.map((origin) => (
|
||||
@@ -1093,7 +1224,7 @@ function AddDatasourceDialog({
|
||||
</FormField>
|
||||
) : (
|
||||
<>
|
||||
<FormField label="Update existing datasource">
|
||||
<FormField label="Update existing datasource" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<select value={targetRef} onChange={(event) => chooseTarget(event.target.value)}>
|
||||
<option value="">Create a new datasource</option>
|
||||
{datasources.filter((item) => item.mode !== "live" && item.shape === "tabular").map((item) => (
|
||||
@@ -1111,10 +1242,10 @@ function AddDatasourceDialog({
|
||||
</>
|
||||
)}
|
||||
<div className="datasources-dialog-grid">
|
||||
<FormField label="Name">
|
||||
<FormField label="Name" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Datasource key">
|
||||
<FormField label="Datasource key" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<input
|
||||
value={sourceName}
|
||||
onChange={(event) => setSourceName(event.target.value)}
|
||||
@@ -1127,7 +1258,7 @@ function AddDatasourceDialog({
|
||||
<FormField label="Description">
|
||||
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="Mode">
|
||||
<FormField label="Mode" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||
<SegmentedControl
|
||||
ariaLabel="Datasource mode"
|
||||
width="fill"
|
||||
@@ -1285,6 +1416,22 @@ function parseRows(text: string): Record<string, unknown>[] {
|
||||
return value;
|
||||
}
|
||||
|
||||
function addDatasourceDraftKey(value: {
|
||||
kind: AddKind;
|
||||
format: UploadFormat;
|
||||
mode: "live" | "cached" | "static";
|
||||
originRef: string;
|
||||
targetRef: string;
|
||||
name: string;
|
||||
sourceName: string;
|
||||
description: string;
|
||||
rowsText: string;
|
||||
csvText: string;
|
||||
delimiter: string;
|
||||
}): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user