fix: constrain dashboard metric validation
This commit is contained in:
parent
b7c673bbd7
commit
31604ea935
3 changed files with 83 additions and 6 deletions
|
|
@ -104,6 +104,21 @@ describe("dashboard model validation", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects duplicate status strip item IDs", () => {
|
||||||
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
|
invalid.statusStrips[0].items.push({
|
||||||
|
...invalid.statusStrips[0].items[0],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = validateDashboardDocument(invalid);
|
||||||
|
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
if (!result.valid) {
|
||||||
|
expect(result.errors.join(" ")).toContain("must be unique");
|
||||||
|
expect(result.errors.join(" ")).toContain(invalid.statusStrips[0].items[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects string values for numeric metric kinds", () => {
|
it("rejects string values for numeric metric kinds", () => {
|
||||||
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
invalid.telemetry[0].value.value = "not a percent";
|
invalid.telemetry[0].value.value = "not a percent";
|
||||||
|
|
@ -113,6 +128,24 @@ describe("dashboard model validation", () => {
|
||||||
expect(result.valid).toBe(false);
|
expect(result.valid).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects percent values outside 0 to 100", () => {
|
||||||
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
|
invalid.telemetry[0].value.value = 150;
|
||||||
|
|
||||||
|
const result = validateDashboardDocument(invalid);
|
||||||
|
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects negative values for nonnegative metric kinds", () => {
|
||||||
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
|
invalid.telemetry[0].value = { kind: "latency", value: -20 };
|
||||||
|
|
||||||
|
const result = validateDashboardDocument(invalid);
|
||||||
|
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects contradictory warning and danger thresholds", () => {
|
it("rejects contradictory warning and danger thresholds", () => {
|
||||||
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
invalid.telemetry[0].thresholds = { warning: 90, danger: 80 };
|
invalid.telemetry[0].thresholds = { warning: 90, danger: 80 };
|
||||||
|
|
@ -125,6 +158,18 @@ describe("dashboard model validation", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects percent thresholds above 100", () => {
|
||||||
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
|
invalid.telemetry[0].thresholds = { warning: 99, danger: 999 };
|
||||||
|
|
||||||
|
const result = validateDashboardDocument(invalid);
|
||||||
|
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
if (!result.valid) {
|
||||||
|
expect(result.errors.join(" ")).toContain("percent thresholds");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects thresholds on text metric values", () => {
|
it("rejects thresholds on text metric values", () => {
|
||||||
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
const invalid = JSON.parse(JSON.stringify(genericDashboardFixture));
|
||||||
invalid.telemetry[0].value = { kind: "text", value: "available" };
|
invalid.telemetry[0].value = { kind: "text", value: "available" };
|
||||||
|
|
|
||||||
|
|
@ -64,16 +64,25 @@ const DatasourceReferenceSchema = Type.Union([
|
||||||
ExternalDatasourceSchema,
|
ExternalDatasourceSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const NumericMetricValueSchema = Type.Object(
|
const PercentMetricValueSchema = Type.Object(
|
||||||
|
{
|
||||||
|
kind: Type.Literal("percent"),
|
||||||
|
value: Type.Number({ minimum: 0, maximum: 100 }),
|
||||||
|
unit: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
|
precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const NonNegativeMetricValueSchema = Type.Object(
|
||||||
{
|
{
|
||||||
kind: Type.Union([
|
kind: Type.Union([
|
||||||
Type.Literal("percent"),
|
|
||||||
Type.Literal("bytes"),
|
Type.Literal("bytes"),
|
||||||
Type.Literal("temperature"),
|
Type.Literal("temperature"),
|
||||||
Type.Literal("latency"),
|
Type.Literal("latency"),
|
||||||
Type.Literal("number"),
|
Type.Literal("number"),
|
||||||
]),
|
]),
|
||||||
value: Type.Number(),
|
value: Type.Number({ minimum: 0 }),
|
||||||
unit: Type.Optional(Type.String({ minLength: 1 })),
|
unit: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })),
|
precision: Type.Optional(Type.Integer({ minimum: 0, maximum: 4 })),
|
||||||
},
|
},
|
||||||
|
|
@ -90,14 +99,15 @@ const TextMetricValueSchema = Type.Object(
|
||||||
);
|
);
|
||||||
|
|
||||||
const MetricValueSchema = Type.Union([
|
const MetricValueSchema = Type.Union([
|
||||||
NumericMetricValueSchema,
|
PercentMetricValueSchema,
|
||||||
|
NonNegativeMetricValueSchema,
|
||||||
TextMetricValueSchema,
|
TextMetricValueSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ThresholdSchema = Type.Object(
|
const ThresholdSchema = Type.Object(
|
||||||
{
|
{
|
||||||
warning: Type.Optional(Type.Number()),
|
warning: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
danger: Type.Optional(Type.Number()),
|
danger: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false, minProperties: 1 },
|
{ additionalProperties: false, minProperties: 1 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,14 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.statusStrips.forEach((strip, stripIndex) => {
|
||||||
|
collectDuplicateIdIssues(
|
||||||
|
`statusStrips/${stripIndex}/items`,
|
||||||
|
strip.items,
|
||||||
|
issues,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
collectMissingReferenceIssues(
|
collectMissingReferenceIssues(
|
||||||
"layout/telemetry",
|
"layout/telemetry",
|
||||||
document.layout.telemetry,
|
document.layout.telemetry,
|
||||||
|
|
@ -171,6 +179,20 @@ function validateSemanticRules(document: DashboardDocument): SemanticValidationI
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
card.value.kind === "percent" &&
|
||||||
|
((card.thresholds?.warning !== undefined && card.thresholds.warning > 100) ||
|
||||||
|
(card.thresholds?.danger !== undefined && card.thresholds.danger > 100))
|
||||||
|
) {
|
||||||
|
issues.push(
|
||||||
|
semanticIssue(
|
||||||
|
`/telemetry/${index}/thresholds`,
|
||||||
|
"percent thresholds must be between 0 and 100",
|
||||||
|
{ id: card.id },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const warning = card.thresholds?.warning;
|
const warning = card.thresholds?.warning;
|
||||||
const danger = card.thresholds?.danger;
|
const danger = card.thresholds?.danger;
|
||||||
if (warning !== undefined && danger !== undefined && warning > danger) {
|
if (warning !== undefined && danger !== undefined && warning > danger) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue