Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:31 +02:00
parent c16bb51aa3
commit 32a32c9906
2 changed files with 107 additions and 314 deletions
+106 -113
View File
@@ -23,14 +23,24 @@ import { FormGrid, DialogSection, ToolbarGroup, ActionToolbar,
ActionBlockerHint,
Button,
ConfirmDialog,
ContentSection,
Dialog,
DocumentationHelpLink,
DismissibleAlert,
FilterBar,
FormField,
IconButton,
LoadingFrame,
MetricCard,
MetricGrid,
SegmentedControl,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge,
WorkspaceFrame,
WorkspaceLayout,
hasScope,
isApiError,
useUnsavedChanges,
@@ -284,10 +294,18 @@ export default function DatasourcesPage({
};
return (
<main className="datasources-page">
<div className="datasources-shell">
<aside className="datasources-sidebar" aria-label="Datasource catalogue">
<ActionToolbar className="datasources-sidebar-toolbar">
<WorkspaceFrame as="main" height="viewport" surface="plain" className="datasources-page" label="Datasource workspace">
<WorkspaceLayout
variant="split"
primarySize="compact"
surface="contained"
primaryScrollable={false}
contentScrollable={false}
primaryLabel="Datasource catalogue"
contentLabel="Datasource workspace"
contentClassName="datasources-workspace"
primary={<>
<ActionToolbar surface="panel-header">
<strong>Data</strong>
<ToolbarGroup align="end" className="datasources-toolbar-actions">
<IconButton
@@ -326,7 +344,7 @@ export default function DatasourcesPage({
}}
/>
</div>
<div className="datasources-search">
<FilterBar surface="panel">
<input
type="search"
value={search}
@@ -334,71 +352,56 @@ export default function DatasourcesPage({
placeholder={`Search ${view}`}
aria-label={`Search ${view}`}
/>
</div>
</FilterBar>
<LoadingFrame
loading={loading}
className="datasources-list-frame"
>
<div className="datasources-list">
<SelectionList variant="navigation" label="Datasources">
{view === "catalogue" ? visibleDatasources.map((item) => (
<button
<SelectionListItem
key={item.ref}
type="button"
className={item.ref === selectedDatasourceRef ? "is-selected" : ""}
selected={item.ref === selectedDatasourceRef}
onClick={() => setSelectedDatasourceRef(item.ref)}
>
<span>
<strong>{item.name}</strong>
<small>{item.source_name} · {item.kind}</small>
</span>
<SelectionListItemContent title={item.name} description={`${item.source_name} · ${item.kind}`} />
<StatusBadge status={item.mode} label={item.mode} />
</button>
</SelectionListItem>
)) : null}
{view === "staging" ? visibleStages.map((item) => (
<button
<SelectionListItem
key={item.ref}
type="button"
className={item.ref === selectedStageRef ? "is-selected" : ""}
selected={item.ref === selectedStageRef}
onClick={() => setSelectedStageRef(item.ref)}
>
<span>
<strong>{item.name}</strong>
<small>{item.source_name} · {formatCount(item.row_count, "row")}</small>
</span>
<SelectionListItemContent title={item.name} description={`${item.source_name} · ${formatCount(item.row_count, "row")}`} />
<StatusBadge status={item.state} label={item.state} />
</button>
</SelectionListItem>
)) : null}
{view === "origins" ? visibleOrigins.map((item) => (
<button
<SelectionListItem
key={item.ref}
type="button"
className={item.ref === selectedOriginRef ? "is-selected" : ""}
selected={item.ref === selectedOriginRef}
onClick={() => setSelectedOriginRef(item.ref)}
>
<span>
<strong>{item.name}</strong>
<small>{item.provider} · {item.kind}</small>
</span>
<SelectionListItemContent title={item.name} description={`${item.provider} · ${item.kind}`} />
<StatusBadge status={item.shape} label={item.shape} />
</button>
</SelectionListItem>
)) : null}
{view === "catalogue" && !visibleDatasources.length ? (
<div className="datasources-list-empty">No datasources</div>
<StatePanel size="compact" description="No datasources" />
) : null}
{view === "staging" && !visibleStages.length ? (
<div className="datasources-list-empty">No staged data</div>
<StatePanel size="compact" description="No staged data" />
) : null}
{view === "origins" && !visibleOrigins.length ? (
<div className="datasources-list-empty">
{originsAvailable ? "No connector origins" : "Connectors unavailable"}
</div>
<StatePanel size="compact" description={originsAvailable ? "No connector origins" : "Connectors unavailable"} />
) : null}
</div>
</SelectionList>
</LoadingFrame>
</aside>
<section className="datasources-workspace">
<ActionToolbar className="datasources-workspace-toolbar">
</>}
>
<ActionToolbar surface="panel-header" className="datasources-workspace-toolbar">
<span className="datasources-current-title">
{view === "catalogue" ? <DatabaseZap size={19} />
: view === "staging" ? <Layers3 size={19} />
@@ -522,8 +525,7 @@ export default function DatasourcesPage({
) : null}
</div>
{working ? <div className="datasources-working" role="status">Working...</div> : null}
</section>
</div>
</WorkspaceLayout>
<AddDatasourceDialog
open={addOpen}
@@ -607,7 +609,7 @@ export default function DatasourcesPage({
onCancel={() => setRetireOpen(false)}
onConfirm={() => void retireSelected()}
/>
</main>
</WorkspaceFrame>
);
}
@@ -624,24 +626,24 @@ function DatasourceDetail({
}) {
return (
<>
<div className="datasources-metrics">
<Metric label="Mode" value={datasource.mode} />
<Metric label="Rows" value={formatNumber(datasource.row_count)} />
<Metric label="Fields" value={String(datasource.schema.length)} />
<Metric label="Revisions" value={String(materializations.length)} />
<Metric label="Updated" value={formatDate(datasource.updated_at)} />
</div>
<MetricGrid columns={5} density="compact" minimum="compact">
<MetricCard density="compact" label="Mode" value={datasource.mode} valueTitle={datasource.mode} />
<MetricCard density="compact" label="Rows" value={formatNumber(datasource.row_count)} />
<MetricCard density="compact" label="Fields" value={String(datasource.schema.length)} />
<MetricCard density="compact" label="Revisions" value={String(materializations.length)} />
<MetricCard density="compact" label="Updated" value={formatDate(datasource.updated_at)} />
</MetricGrid>
{datasource.description ? (
<div className="datasources-description">{datasource.description}</div>
) : null}
<section className="datasources-detail-section">
<div className="datasources-section-heading">
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span><ShieldCheck size={16} /> Governance</span>
<StatusBadge
status={datasource.governance.publication_state}
label={datasource.governance.publication_state}
/>
</div>
</ActionToolbar>
<div className="datasources-key-values">
<span><small>Authority</small><strong>{readableToken(datasource.governance.authority_mode)}</strong></span>
<span><small>Classification</small><strong>{datasource.governance.classification}</strong></span>
@@ -653,25 +655,25 @@ function DatasourceDetail({
{datasource.governance.semantic_definition ? (
<p className="datasources-dialog-copy">{datasource.governance.semantic_definition}</p>
) : null}
</section>
<section className="datasources-detail-section">
<div className="datasources-section-heading">
</ContentSection>
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span><Eye size={16} /> Preview</span>
<small>{preview ? `${formatNumber(preview.total_rows)} total rows` : ""}</small>
</div>
</ActionToolbar>
{loading ? (
<div className="datasources-inline-loading">Loading preview...</div>
) : preview ? (
<PreviewTable datasource={datasource} rows={preview.rows} />
) : (
<div className="datasources-inline-empty">No preview available</div>
<StatePanel size="inline" description="No preview available" />
)}
</section>
<section className="datasources-detail-section">
<div className="datasources-section-heading">
</ContentSection>
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span><Archive size={16} /> Materializations</span>
<small>Immutable revisions</small>
</div>
</ActionToolbar>
<div className="datasources-table-scroll">
<table>
<thead>
@@ -708,14 +710,14 @@ function DatasourceDetail({
</tbody>
</table>
</div>
</section>
<section className="datasources-detail-section">
<div className="datasources-section-heading">
</ContentSection>
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Schema</span>
<small>Version {datasource.schema_version}</small>
</div>
</ActionToolbar>
<SchemaTable fields={datasource.schema} />
</section>
</ContentSection>
</>
);
}
@@ -727,21 +729,21 @@ function StageDetail({ stage }: { stage: DatasourceStage }) {
const schemaClassification = stage.validation.schema_change?.classification ?? "new";
return (
<>
<div className="datasources-metrics">
<Metric label="State" value={stage.state} />
<Metric label="Mode" value={stage.mode} />
<Metric label="Rows" value={formatNumber(stage.row_count)} />
<Metric label="Fields" value={String(stage.schema.length)} />
<Metric label="Created" value={formatDate(stage.created_at)} />
</div>
<section className="datasources-detail-section">
<div className="datasources-section-heading">
<MetricGrid columns={5} density="compact" minimum="compact">
<MetricCard density="compact" label="State" value={stage.state} valueTitle={stage.state} />
<MetricCard density="compact" label="Mode" value={stage.mode} valueTitle={stage.mode} />
<MetricCard density="compact" label="Rows" value={formatNumber(stage.row_count)} />
<MetricCard density="compact" label="Fields" value={String(stage.schema.length)} />
<MetricCard density="compact" label="Created" value={formatDate(stage.created_at)} />
</MetricGrid>
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Validation</span>
<StatusBadge
status={stage.validation.valid === false ? "invalid" : "ready"}
label={stage.validation.valid === false ? "Invalid" : "Ready"}
/>
</div>
</ActionToolbar>
<div className="datasources-key-values">
<span><small>Fingerprint</small><strong>{shortFingerprint(stage.fingerprint)}</strong></span>
<span><small>Target</small><strong>{stage.target_datasource_ref || "New datasource"}</strong></span>
@@ -770,27 +772,27 @@ function StageDetail({ stage }: { stage: DatasourceStage }) {
All configured quality rules passed and no blocking schema change was detected.
</div>
)}
</section>
</ContentSection>
{schemaChanges.length ? (
<section className="datasources-detail-section">
<div className="datasources-section-heading">
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Schema comparison</span>
<StatusBadge status={schemaClassification} label={readableToken(schemaClassification)} />
</div>
</ActionToolbar>
<ul className="datasources-schema-changes">
{schemaChanges.map((change, index) => (
<SchemaChangeItem key={`${change.code}-${change.field ?? index}`} change={change} />
))}
</ul>
</section>
</ContentSection>
) : null}
<section className="datasources-detail-section">
<div className="datasources-section-heading">
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Detected schema</span>
<small>{stage.shape}</small>
</div>
</ActionToolbar>
<SchemaTable fields={stage.schema} />
</section>
</ContentSection>
</>
);
}
@@ -834,22 +836,22 @@ function SchemaChangeItem({ change }: { change: DatasourceSchemaChange }) {
function OriginDetail({ origin }: { origin: DatasourceOrigin }) {
return (
<>
<div className="datasources-metrics">
<Metric label="Provider" value={origin.provider} />
<Metric label="Mode" value={readableToken(origin.source_mode)} />
<Metric label="Health" value={readableToken(origin.health.status)} />
<Metric label="Rows" value={formatNumber(origin.row_count)} />
<Metric label="Fields" value={String(origin.schema.length)} />
<Metric label="Updated" value={formatDate(origin.updated_at)} />
</div>
<MetricGrid columns="auto" density="compact" minimum="compact">
<MetricCard density="compact" label="Provider" value={origin.provider} valueTitle={origin.provider} />
<MetricCard density="compact" label="Mode" value={readableToken(origin.source_mode)} />
<MetricCard density="compact" label="Health" value={readableToken(origin.health.status)} />
<MetricCard density="compact" label="Rows" value={formatNumber(origin.row_count)} />
<MetricCard density="compact" label="Fields" value={String(origin.schema.length)} />
<MetricCard density="compact" label="Updated" value={formatDate(origin.updated_at)} />
</MetricGrid>
{origin.description ? (
<div className="datasources-description">{origin.description}</div>
) : null}
<section className="datasources-detail-section">
<div className="datasources-section-heading">
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Registration options</span>
<small>{origin.supported_modes.join(", ")}</small>
</div>
</ActionToolbar>
<div className="datasources-key-values">
<span><small>Origin reference</small><strong>{origin.ref}</strong></span>
<span><small>Kind</small><strong>{readableToken(origin.kind)}</strong></span>
@@ -858,14 +860,14 @@ function OriginDetail({ origin }: { origin: DatasourceOrigin }) {
<span><small>Health</small><strong>{origin.health.summary}</strong></span>
<span><small>Pushdown</small><strong>{pushdownSummary(origin)}</strong></span>
</div>
</section>
<section className="datasources-detail-section">
<div className="datasources-section-heading">
</ContentSection>
<ContentSection>
<ActionToolbar surface="section-header" className="datasources-section-heading">
<span>Discovered schema</span>
<small>Version {origin.schema_version}</small>
</div>
</ActionToolbar>
<SchemaTable fields={origin.schema} />
</section>
</ContentSection>
</>
);
}
@@ -1479,17 +1481,8 @@ function SchemaTable({ fields }: { fields: Datasource["schema"] }) {
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<span>
<small>{label}</small>
<strong title={value}>{value}</strong>
</span>
);
}
function EmptyWorkspace({ icon, label }: { icon: React.ReactNode; label: string }) {
return <div className="datasources-workspace-empty">{icon}<strong>{label}</strong></div>;
return <StatePanel size="fill" icon={icon} title={label} />;
}
function filterItems<T>(