Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions web/src/modules/swiss/components/ReleaseObjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ function ScalerPanel({
);
const current = status?.currentReplicas;
const desired = status?.desiredReplicas;
const failing = status?.conditions?.find((c) => c.status === "False");
const failing = scalerFailing(status?.conditions);
const custom = spec?.metricProvider === "Custom";

let state: React.ReactNode = null;
Expand Down Expand Up @@ -349,7 +349,7 @@ function ScalerPanel({
</div>
)}

<Conditions conditions={status?.conditions} />
<Conditions conditions={status?.conditions} neutral={scalerNeutral(status?.conditions)} />
</>
)}
</ObjectCard>
Expand Down Expand Up @@ -681,7 +681,18 @@ function Stale({
);
}

export function Conditions({ conditions }: { conditions?: Condition[] }) {
// Only AbleToScale=False or ScalingActive=False (DecisionAvailable on operator 0.5.0) is a scaler failure.
export function scalerFailing(conditions?: Condition[]): Condition | undefined {
const of = (type: string) => conditions?.find((c) => c.type === type);
return [of("AbleToScale"), of("ScalingActive") ?? of("DecisionAvailable")].find((c) => c?.status === "False");
}

// ScalingLimited is informational; DecisionAvailable only mirrors ScalingActive when that is present.
function scalerNeutral(conditions?: Condition[]): string[] {
return conditions?.some((c) => c.type === "ScalingActive") ? ["ScalingLimited", "DecisionAvailable"] : ["ScalingLimited"];
}

export function Conditions({ conditions, neutral = [] }: { conditions?: Condition[]; neutral?: string[] }) {
if (!conditions?.length) return null;
return (
<ul className="mt-auto space-y-1.5 border-t pt-3">
Expand All @@ -691,11 +702,13 @@ export function Conditions({ conditions }: { conditions?: Condition[] }) {
<span
className={cn(
"size-1.5 shrink-0 rounded-full",
c.status === "True"
? "bg-success"
: c.status === "False"
? "bg-warning"
: "bg-muted-foreground",
neutral.includes(c.type)
? "bg-muted-foreground"
: c.status === "True"
? "bg-success"
: c.status === "False"
? "bg-warning"
: "bg-muted-foreground",
)}
/>
<span className="font-medium">{c.type}</span>
Expand Down
23 changes: 23 additions & 0 deletions web/src/modules/swiss/components/scaler-conditions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { scalerFailing } from "@swiss/components/ReleaseObjects";

describe("scalerFailing", () => {
it("does not treat ScalingLimited as a failure", () => {
expect(scalerFailing([
{ type: "ScalingActive", status: "True", reason: "DecisionReceived" },
{ type: "AbleToScale", status: "True", reason: "Synced" },
{ type: "ScalingLimited", status: "False", reason: "DesiredWithinRange" },
])).toBeUndefined();
});
it("fails on AbleToScale=False or ScalingActive=False", () => {
expect(scalerFailing([{ type: "AbleToScale", status: "False", reason: "TargetNotFound" }])?.reason).toBe("TargetNotFound");
expect(scalerFailing([{ type: "ScalingActive", status: "False", reason: "InvalidSpec" }])?.reason).toBe("InvalidSpec");
});
it("uses DecisionAvailable only when ScalingActive is absent", () => {
expect(scalerFailing([{ type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" }])?.reason).toBe("DecisionUnavailable");
expect(scalerFailing([
{ type: "ScalingActive", status: "True", reason: "MetricsAvailable" },
{ type: "DecisionAvailable", status: "False", reason: "DecisionUnavailable" },
])).toBeUndefined();
});
});
4 changes: 2 additions & 2 deletions web/src/modules/swiss/routes/DeploymentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { Dialog } from "@swiss/components/ui/dialog";
import { Field, Input } from "@swiss/components/ui/input";
import { CatalogModelDiff } from "@swiss/components/CatalogModelDiff";
import { Provenance } from "@swiss/components/Provenance";
import { ReleaseObjects, pick, useReleaseObjects } from "@swiss/components/ReleaseObjects";
import { ReleaseObjects, pick, scalerFailing, useReleaseObjects } from "@swiss/components/ReleaseObjects";
import { AccessPoint } from "@swiss/components/Endpoint";
import { ReleaseStatus } from "@swiss/components/ReleaseStatus";
import { SLODialog } from "@swiss/components/SLOCard";
Expand Down Expand Up @@ -342,7 +342,7 @@ function scalerStep({ result, status }: ReturnType<typeof pick<LLMScalerSpec, LL
if (!result) return { label, state: "off", detail: "fixed replicas" };
if (result.error) return { label, state: "wait", detail: "unreadable" };
if (result.missing) return { label, state: "bad", detail: "LLMScaler deleted" };
const failing = status?.conditions?.find((c) => c.status === "False");
const failing = scalerFailing(status?.conditions);
if (failing) return { label, state: "bad", detail: failing.reason || failing.type };
const cur = status?.currentReplicas;
const want = status?.desiredReplicas;
Expand Down
Loading