feat: define dashboard model schema #15
3 changed files with 72 additions and 3 deletions
|
|
@ -140,16 +140,17 @@ function service(
|
|||
icon: string,
|
||||
href?: string,
|
||||
) {
|
||||
return {
|
||||
const entry = {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
icon,
|
||||
link: href ? { href, external: true } : undefined,
|
||||
severity: "ok" as const,
|
||||
detail: "ready",
|
||||
datasource: { type: "placeholder" as const, reason: "service health adapter pending" },
|
||||
};
|
||||
|
||||
return href ? { ...entry, link: { href, external: true } } : entry;
|
||||
}
|
||||
|
||||
function group(id: string, title: string, services: ReturnType<typeof service>[]) {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,31 @@ describe("dashboard model validation", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("rejects thresholds on text metric values", () => {
|
||||
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||
invalid.telemetry[0].value = { kind: "text", value: "available" };
|
||||
invalid.telemetry[0].thresholds = { warning: 10 };
|
||||
|
||||
const result = validateDashboardDocument(invalid);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors.join(" ")).toContain("text metric values");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects undefined properties because they are not JSON values", () => {
|
||||
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||
invalid.serviceGroups[0].services[0].link = undefined;
|
||||
|
||||
const result = validateDashboardDocument(invalid);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors.join(" ")).toContain("must be omitted instead of undefined");
|
||||
}
|
||||
});
|
||||
|
||||
it("exports JSON Schema for external tool contracts", () => {
|
||||
expect(dashboardDocumentJsonSchema.$id).toContain("dashboard-document.v1");
|
||||
expect(dashboardDocumentJsonSchema.properties).toHaveProperty("schemaVersion");
|
||||
|
|
|
|||
|
|
@ -55,11 +55,14 @@ export function validateDashboardDocument(
|
|||
value: unknown,
|
||||
): DashboardValidationResult {
|
||||
const finiteNumberIssues: SemanticValidationIssue[] = [];
|
||||
const undefinedIssues: SemanticValidationIssue[] = [];
|
||||
collectFiniteNumberIssues(value, "", finiteNumberIssues);
|
||||
collectUndefinedIssues(value, "", undefinedIssues);
|
||||
|
||||
if (validateDashboard(value)) {
|
||||
const semanticIssues = [
|
||||
...finiteNumberIssues,
|
||||
...undefinedIssues,
|
||||
...validateSemanticRules(value),
|
||||
];
|
||||
if (semanticIssues.length === 0) {
|
||||
|
|
@ -73,7 +76,11 @@ export function validateDashboardDocument(
|
|||
};
|
||||
}
|
||||
|
||||
const details = [...(validateDashboard.errors || []), ...finiteNumberIssues];
|
||||
const details = [
|
||||
...(validateDashboard.errors || []),
|
||||
...finiteNumberIssues,
|
||||
...undefinedIssues,
|
||||
];
|
||||
return {
|
||||
valid: false,
|
||||
errors: formatValidationErrors(details),
|
||||
|
|
@ -154,6 +161,16 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI
|
|||
);
|
||||
|
||||
document.telemetry.forEach((card, index) => {
|
||||
if (card.value.kind === "text" && card.thresholds !== undefined) {
|
||||
issues.push(
|
||||
semanticIssue(
|
||||
`/telemetry/${index}/thresholds`,
|
||||
"must not be set for text metric values",
|
||||
{ id: card.id },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const warning = card.thresholds?.warning;
|
||||
const danger = card.thresholds?.danger;
|
||||
if (warning !== undefined && danger !== undefined && warning > danger) {
|
||||
|
|
@ -170,6 +187,32 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI
|
|||
return issues;
|
||||
}
|
||||
|
||||
function collectUndefinedIssues(
|
||||
value: unknown,
|
||||
path: string,
|
||||
issues: SemanticValidationIssue[],
|
||||
) {
|
||||
if (value === undefined) {
|
||||
issues.push(
|
||||
semanticIssue(path || "/", "must be omitted instead of undefined", {}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
collectUndefinedIssues(item, `${path}/${index}`, issues);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
Object.entries(value).forEach(([key, item]) => {
|
||||
collectUndefinedIssues(item, `${path}/${escapeJsonPointer(key)}`, issues);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function collectFiniteNumberIssues(
|
||||
value: unknown,
|
||||
path: string,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue