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
1 change: 1 addition & 0 deletions changelog/161.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Loading more passing groups on the failures page no longer hides the groups already loaded when it fails, and a failure to load the model list is now reported with a retry.
1 change: 1 addition & 0 deletions changelog/161.improvement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The diagnostic dots on the failures page now turn grey and gently swell while the counts for new filters load.
19 changes: 19 additions & 0 deletions frontend/src/components/failures/health.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { HEALTH, HealthDot } from "./health";

describe("HealthDot", () => {
it("shows the health colour", () => {
render(<HealthDot health="failed" />);
const dot = screen.getByRole("img", { name: "All failing" });
expect(dot).toHaveClass(HEALTH.failed.colour);
expect(dot).not.toHaveClass("motion-safe:animate-swell");
});

it("goes grey and swells while pending, hiding the stale health", () => {
render(<HealthDot health="failed" pending />);
const dot = screen.getByRole("img", { name: "Updating" });
expect(dot).toHaveClass(HEALTH.not_run.colour, "motion-safe:animate-swell");
expect(dot).not.toHaveClass(HEALTH.failed.colour);
});
});
13 changes: 11 additions & 2 deletions frontend/src/components/failures/health.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type React from "react";
import type { DiagnosticHealth } from "@/lib/failures";
import { cn } from "@/lib/utils";

Expand Down Expand Up @@ -28,20 +29,28 @@ export const HEALTH = {
{ label: string; description: string; colour: string }
>;

/** A diagnostic's health, or a grey swelling dot while `pending` new counts */
export function HealthDot({
health,
pending = false,
className,
style,
}: {
health: DiagnosticHealth;
pending?: boolean;
className?: string;
style?: React.CSSProperties;
}) {
return (
<span
role="img"
aria-label={HEALTH[health].label}
aria-label={pending ? "Updating" : HEALTH[health].label}
style={style}
className={cn(
"inline-block size-3 shrink-0 rounded-full",
HEALTH[health].colour,
pending
? cn(HEALTH.not_run.colour, "motion-safe:animate-swell")
: HEALTH[health].colour,
className,
)}
/>
Expand Down
50 changes: 33 additions & 17 deletions frontend/src/components/failures/passingGroups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,37 @@ function PassingGroupList({
mipEra: MipEra;
model?: string;
}) {
const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
...executionsListRecentExecutionGroupsInfiniteOptions({
query: {
successful: true,
provider_slug: diagnostic.provider.slug,
diagnostic_slug: diagnostic.slug,
mip_era: mipEra,
source_id: model,
limit: PAGE_SIZE,
},
}),
initialPageParam: 0,
getNextPageParam: (_lastPage, allPages) => nextOffset(allPages),
});
const {
data,
error,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
...executionsListRecentExecutionGroupsInfiniteOptions({
query: {
successful: true,
provider_slug: diagnostic.provider.slug,
diagnostic_slug: diagnostic.slug,
mip_era: mipEra,
source_id: model,
limit: PAGE_SIZE,
},
}),
initialPageParam: 0,
getNextPageParam: (_lastPage, allPages) => nextOffset(allPages),
});

if (error) {
return <p className="text-sm text-destructive">{apiErrorMessage(error)}</p>;
if (!data && error) {
return (
<div className="space-y-2">
<p className="text-sm text-destructive">{apiErrorMessage(error)}</p>
<Button variant="outline" size="sm" onClick={() => refetch()}>
Try again
</Button>
</div>
);
}
if (!data) {
return (
Expand All @@ -90,6 +103,9 @@ function PassingGroupList({
<PassingGroupItem key={group.id} group={group} />
))}
</ul>
{error && (
<p className="text-sm text-destructive">{apiErrorMessage(error)}</p>
)}
{hasNextPage && (
<Button
variant="ghost"
Expand Down
44 changes: 36 additions & 8 deletions frontend/src/components/failures/providerHealthOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,32 @@ function groupByProvider(diagnostics: DiagnosticFailures[]): ProviderGroup[] {
return [...groups.values()];
}

/** Offsets each dot's swell so a row ripples rather than pulsing in step */
const SWELL_STAGGER_MS = 60;

function DiagnosticDot({
failures,
filters,
updating,
index,
}: {
failures: DiagnosticFailures;
filters?: FailureFilters;
updating: boolean;
index: number;
}) {
const { diagnostic } = failures;
const dot = <HealthDot health={failures.health} />;
const dot = (
<HealthDot
health={failures.health}
pending={updating}
style={
updating
? { animationDelay: `${index * SWELL_STAGGER_MS}ms` }
: undefined
}
/>
);

// A failing diagnostic jumps to its section below, the others open the diagnostic
return (
Expand Down Expand Up @@ -69,12 +86,18 @@ function DiagnosticDot({
</TooltipTrigger>
<TooltipContent>
<p className="font-medium">{diagnostic.name}</p>
<p>
{failures.total === 0
? "No execution groups"
: failureSummary(failures, filters)}
</p>
<p className="opacity-80">{HEALTH[failures.health].label}</p>
{updating ? (
<p className="opacity-80">Updating…</p>
) : (
<>
<p>
{failures.total === 0
? "No execution groups"
: failureSummary(failures, filters)}
</p>
<p className="opacity-80">{HEALTH[failures.health].label}</p>
</>
)}
</TooltipContent>
</Tooltip>
);
Expand All @@ -84,10 +107,13 @@ function DiagnosticDot({
export function ProviderHealthOverview({
diagnostics,
filters,
updating = false,
}: {
diagnostics: DiagnosticFailures[];
/** What the failures were narrowed to, named in each dot's summary */
filters?: FailureFilters;
/** The counts are from the previous filters while new ones load, so the dots go grey and swell */
updating?: boolean;
}) {
return (
<div className="space-y-4">
Expand All @@ -110,11 +136,13 @@ export function ProviderHealthOverview({
</p>
</div>
<div className="flex flex-wrap gap-1">
{diagnostics.map((failures) => (
{diagnostics.map((failures, index) => (
<DiagnosticDot
key={failures.diagnostic.slug}
failures={failures}
filters={filters}
updating={updating}
index={index}
/>
))}
</div>
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/failures/useFailures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,21 @@ export function useFailures(mipEra: MipEra, model?: string) {
});

// A failed query that is being retried counts as loading again
const failed = [catalog, unsuccessful].filter(
(query) => query.isError && !query.isFetching,
);
const failedQuery = (query: { isError: boolean; isFetching: boolean }) =>
query.isError && !query.isFetching;
const failed = [catalog, unsuccessful, models].filter(failedQuery);
const error = failed.length > 0 ? failed[0].error : null;
const loaded = catalog.data !== undefined && unsuccessful.data !== undefined;
// The models only fill in the model picker, so their failure is reported without hiding the page
const pageFailed = [catalog, unsuccessful].some(failedQuery);

return {
catalog: catalog.data?.data,
unsuccessful: unsuccessful.data,
models: models.data?.data.map(({ source_id }) => source_id),
isLoading: !loaded && error === null,
isLoading: !loaded && !pageFailed,
/** The catalog still holds the previous filters' counts while the new ones load */
isUpdating: catalog.isPlaceholderData,
error,
retry: () => {
for (const query of failed) {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/routes/_app/executions.failures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function FailuresPage() {
const search = Route.useSearch();
const { model, reason, from, to, passing = false } = search;
const { mipEra } = useMipEra(search.mip_era);
const { catalog, unsuccessful, models, isLoading, error, retry } =
const { catalog, unsuccessful, models, isLoading, isUpdating, error, retry } =
useFailures(mipEra, model);
const navigate = useNavigate({ from: Route.fullPath });
const updateSearch = (patch: Partial<typeof search>) =>
Expand Down Expand Up @@ -215,6 +215,7 @@ function FailuresPage() {
<ProviderHealthOverview
diagnostics={diagnostics}
filters={filters}
updating={isUpdating}
/>
</CardContent>
</Card>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@
--color-cmip-blue: #000546;
--color-cmip-mustard: #f2b30d;
--color-cmip-cyan: #22bfb7;

--animate-swell: swell 1.6s ease-in-out infinite;
@keyframes swell {
0%,
100% {
transform: scale(1);
opacity: 0.6;
}
50% {
transform: scale(1.3);
opacity: 1;
}
}
}

@layer base {
Expand Down
Loading