Add datasource quality and schema gates
This commit is contained in:
@@ -87,6 +87,44 @@ export type DatasourceMaterialization = {
|
||||
governance: DatasourceGovernance;
|
||||
};
|
||||
|
||||
export type DatasourceValidationDiagnostic = {
|
||||
severity: "error" | "warning";
|
||||
code: string;
|
||||
message: string;
|
||||
rule_id?: string;
|
||||
affected_rows?: number;
|
||||
row_numbers?: number[];
|
||||
row_numbers_truncated?: boolean;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DatasourceSchemaChange = {
|
||||
code: string;
|
||||
classification: "compatible" | "warning" | "breaking";
|
||||
message: string;
|
||||
field?: string;
|
||||
before?: DatasourceField;
|
||||
after?: DatasourceField;
|
||||
};
|
||||
|
||||
export type DatasourceStageValidation = {
|
||||
version?: number;
|
||||
policy_version?: string;
|
||||
policy_hash?: string;
|
||||
valid?: boolean;
|
||||
errors?: DatasourceValidationDiagnostic[];
|
||||
warnings?: DatasourceValidationDiagnostic[];
|
||||
quality?: {
|
||||
rules_evaluated?: number;
|
||||
rules_passed?: number;
|
||||
rules_failed?: number;
|
||||
};
|
||||
schema_change?: {
|
||||
classification?: "new" | "compatible" | "warning" | "breaking";
|
||||
changes?: DatasourceSchemaChange[];
|
||||
};
|
||||
};
|
||||
|
||||
export type DatasourceStage = {
|
||||
ref: string;
|
||||
name: string;
|
||||
@@ -100,7 +138,7 @@ export type DatasourceStage = {
|
||||
schema: DatasourceField[];
|
||||
row_count?: number | null;
|
||||
byte_count?: number | null;
|
||||
validation: Record<string, unknown>;
|
||||
validation: DatasourceStageValidation;
|
||||
created_at?: string | null;
|
||||
promoted_at?: string | null;
|
||||
promoted_materialization_ref?: string | null;
|
||||
|
||||
@@ -56,7 +56,9 @@ import {
|
||||
type DatasourceMaterialization,
|
||||
type DatasourceOrigin,
|
||||
type DatasourcePreview,
|
||||
type DatasourceStage
|
||||
type DatasourceSchemaChange,
|
||||
type DatasourceStage,
|
||||
type DatasourceValidationDiagnostic
|
||||
} from "../../api/datasources";
|
||||
import {
|
||||
DATASOURCE_FIELDS_DOCUMENTATION,
|
||||
@@ -717,6 +719,10 @@ function DatasourceDetail({
|
||||
}
|
||||
|
||||
function StageDetail({ stage }: { stage: DatasourceStage }) {
|
||||
const errors = stage.validation.errors ?? [];
|
||||
const warnings = stage.validation.warnings ?? [];
|
||||
const schemaChanges = stage.validation.schema_change?.changes ?? [];
|
||||
const schemaClassification = stage.validation.schema_change?.classification ?? "new";
|
||||
return (
|
||||
<>
|
||||
<div className="datasources-metrics">
|
||||
@@ -738,8 +744,44 @@ function StageDetail({ stage }: { stage: DatasourceStage }) {
|
||||
<span><small>Fingerprint</small><strong>{shortFingerprint(stage.fingerprint)}</strong></span>
|
||||
<span><small>Target</small><strong>{stage.target_datasource_ref || "New datasource"}</strong></span>
|
||||
<span><small>Promoted revision</small><strong>{stage.promoted_materialization_ref || "Not promoted"}</strong></span>
|
||||
<span><small>Quality policy</small><strong>{stage.validation.policy_version || "Local default"}</strong></span>
|
||||
<span><small>Policy hash</small><strong>{shortFingerprint(stage.validation.policy_hash || "")}</strong></span>
|
||||
<span><small>Schema change</small><strong>{readableToken(schemaClassification)}</strong></span>
|
||||
</div>
|
||||
{errors.length || warnings.length ? (
|
||||
<div className="datasources-validation-alerts">
|
||||
{errors.length ? (
|
||||
<DismissibleAlert tone="danger" compact dismissible={false}>
|
||||
<strong>Promotion blockers</strong>
|
||||
<ValidationDiagnosticList diagnostics={errors} />
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
{warnings.length ? (
|
||||
<DismissibleAlert tone="warning" compact dismissible={false}>
|
||||
<strong>Review warnings</strong>
|
||||
<ValidationDiagnosticList diagnostics={warnings} />
|
||||
</DismissibleAlert>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="datasources-validation-ok">
|
||||
All configured quality rules passed and no blocking schema change was detected.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{schemaChanges.length ? (
|
||||
<section className="datasources-detail-section">
|
||||
<div className="datasources-section-heading">
|
||||
<span>Schema comparison</span>
|
||||
<StatusBadge status={schemaClassification} label={readableToken(schemaClassification)} />
|
||||
</div>
|
||||
<ul className="datasources-schema-changes">
|
||||
{schemaChanges.map((change, index) => (
|
||||
<SchemaChangeItem key={`${change.code}-${change.field ?? index}`} change={change} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
<section className="datasources-detail-section">
|
||||
<div className="datasources-section-heading">
|
||||
<span>Detected schema</span>
|
||||
@@ -751,6 +793,42 @@ function StageDetail({ stage }: { stage: DatasourceStage }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ValidationDiagnosticList({
|
||||
diagnostics
|
||||
}: {
|
||||
diagnostics: DatasourceValidationDiagnostic[];
|
||||
}) {
|
||||
return (
|
||||
<ul className="datasources-diagnostic-list">
|
||||
{diagnostics.map((diagnostic, index) => {
|
||||
const details = [
|
||||
diagnostic.rule_id ? `Rule ${diagnostic.rule_id}` : "",
|
||||
diagnostic.affected_rows !== undefined ? `${diagnostic.affected_rows} affected row${diagnostic.affected_rows === 1 ? "" : "s"}` : "",
|
||||
diagnostic.row_numbers?.length ? `Rows ${diagnostic.row_numbers.join(", ")}${diagnostic.row_numbers_truncated ? ", …" : ""}` : ""
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<li key={`${diagnostic.code}-${diagnostic.rule_id ?? index}`}>
|
||||
<span>{diagnostic.message}</span>
|
||||
{details.length ? <small>{details.join(" · ")}</small> : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function SchemaChangeItem({ change }: { change: DatasourceSchemaChange }) {
|
||||
return (
|
||||
<li>
|
||||
<StatusBadge status={change.classification} label={readableToken(change.classification)} />
|
||||
<span>
|
||||
<strong>{change.message}</strong>
|
||||
{change.field ? <small>{change.field}</small> : null}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function OriginDetail({ origin }: { origin: DatasourceOrigin }) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -62,7 +62,15 @@ const en = {
|
||||
"Publication state": "Publication state",
|
||||
"Semantic definition": "Semantic definition",
|
||||
"Freshness policy (JSON)": "Freshness policy (JSON)",
|
||||
"Quality policy (JSON)": "Quality policy (JSON)"
|
||||
"Quality policy (JSON)": "Quality policy (JSON)",
|
||||
"Quality policy": "Quality policy",
|
||||
"Policy hash": "Policy hash",
|
||||
"Schema change": "Schema change",
|
||||
"Local default": "Local default",
|
||||
"Promotion blockers": "Promotion blockers",
|
||||
"Review warnings": "Review warnings",
|
||||
"All configured quality rules passed and no blocking schema change was detected.": "All configured quality rules passed and no blocking schema change was detected.",
|
||||
"Schema comparison": "Schema comparison"
|
||||
} as const;
|
||||
|
||||
const de: Record<keyof typeof en, string> = {
|
||||
@@ -127,7 +135,15 @@ const de: Record<keyof typeof en, string> = {
|
||||
"Publication state": "Veröffentlichungsstatus",
|
||||
"Semantic definition": "Semantische Definition",
|
||||
"Freshness policy (JSON)": "Aktualitätsrichtlinie (JSON)",
|
||||
"Quality policy (JSON)": "Qualitätsrichtlinie (JSON)"
|
||||
"Quality policy (JSON)": "Qualitätsrichtlinie (JSON)",
|
||||
"Quality policy": "Qualitätsrichtlinie",
|
||||
"Policy hash": "Richtlinien-Hash",
|
||||
"Schema change": "Schemaänderung",
|
||||
"Local default": "Lokaler Standard",
|
||||
"Promotion blockers": "Übernahmehindernisse",
|
||||
"Review warnings": "Prüfhinweise",
|
||||
"All configured quality rules passed and no blocking schema change was detected.": "Alle konfigurierten Qualitätsregeln wurden erfüllt und es wurde keine blockierende Schemaänderung erkannt.",
|
||||
"Schema comparison": "Schemavergleich"
|
||||
};
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||
|
||||
@@ -369,6 +369,74 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.datasources-validation-alerts {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border-top: var(--border-line);
|
||||
}
|
||||
|
||||
.datasources-validation-alerts .alert {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.datasources-diagnostic-list,
|
||||
.datasources-schema-changes {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 7px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.datasources-diagnostic-list li,
|
||||
.datasources-diagnostic-list span,
|
||||
.datasources-diagnostic-list small,
|
||||
.datasources-schema-changes li,
|
||||
.datasources-schema-changes span,
|
||||
.datasources-schema-changes strong,
|
||||
.datasources-schema-changes small {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.datasources-diagnostic-list small,
|
||||
.datasources-schema-changes small {
|
||||
margin-top: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.datasources-validation-ok {
|
||||
padding: 10px 12px;
|
||||
border-top: var(--border-line);
|
||||
color: var(--success);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.datasources-schema-changes {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.datasources-schema-changes li {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 9px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.datasources-schema-changes li + li {
|
||||
border-top: var(--border-line);
|
||||
}
|
||||
|
||||
.datasources-schema-changes strong {
|
||||
color: var(--text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.datasources-workspace-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
Reference in New Issue
Block a user