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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock("next/router", () => ({

describe("useJobCard", () => {
const mockUuid = "test-uuid";
const mockDuration = 3600; // 1 hour in seconds
const mockDuration = 3_600_000; // 1 hour in milliseconds
const mockRouter = {
query: {
experiment: "current-uuid",
Expand All @@ -23,17 +23,13 @@ describe("useJobCard", () => {
});

it("should return the current UUID from router query", () => {
const { result } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: mockDuration })
);
const { result } = renderHook(() => useJobCard({ uuid: mockUuid }));

expect(result.current.currentUUID).toBe("current-uuid");
});

it("should navigate to the job details when handleClick is called", () => {
const { result } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: mockDuration })
);
const { result } = renderHook(() => useJobCard({ uuid: mockUuid }));

act(() => {
result.current.handleClick();
Expand All @@ -49,9 +45,7 @@ describe("useJobCard", () => {
});

it("should return different styles based on selection state", () => {
const { result } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: mockDuration })
);
const { result } = renderHook(() => useJobCard({ uuid: mockUuid }));

const selectedStyle = result.current.getCardStyle(true);
const unselectedStyle = result.current.getCardStyle(false);
Expand All @@ -61,22 +55,21 @@ describe("useJobCard", () => {
});

it("should format job duration correctly", () => {
const { result } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: mockDuration })
);
const { result } = renderHook(() => useJobCard({ uuid: mockUuid }));

const formattedDuration = result.current.formatJobDuration(mockDuration);
const formattedDuration = result.current.formatDurationText(
"progress",
mockDuration
);

// The exact format might vary based on date-fns version and locale,
// so we'll just check that it contains the expected parts
expect(formattedDuration).toContain("Running for");
expect(formattedDuration).toContain("hour");
expect(formattedDuration).toContain("1h");
});

it("should correctly identify progress and suspend statuses", () => {
const { result } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: mockDuration })
);
const { result } = renderHook(() => useJobCard({ uuid: mockUuid }));

expect(result.current.isProgressOrSuspend("progress")).toBe(true);
expect(result.current.isProgressOrSuspend("suspend")).toBe(true);
Expand All @@ -88,23 +81,32 @@ describe("useJobCard", () => {
it("should handle different durations correctly", () => {
// Test with 30 minutes
const { result: result1 } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: 1800 })
useJobCard({ uuid: mockUuid })
);
const formattedDuration1 = result1.current.formatDurationText(
"progress",
1800
);
const formattedDuration1 = result1.current.formatJobDuration(1800);
expect(formattedDuration1).toContain("Running for");

// Test with 2 days
const { result: result2 } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: 172800 })
useJobCard({ uuid: mockUuid })
);
const formattedDuration2 = result2.current.formatDurationText(
"progress",
172800
);
const formattedDuration2 = result2.current.formatJobDuration(172800);
expect(formattedDuration2).toContain("Running for");

// Test with 0 seconds
const { result: result3 } = renderHook(() =>
useJobCard({ uuid: mockUuid, duration: 0 })
useJobCard({ uuid: mockUuid })
);
const formattedDuration3 = result3.current.formatDurationText(
"progress",
0
);
const formattedDuration3 = result3.current.formatJobDuration(0);
expect(formattedDuration3).toContain("Running for");
expect(formattedDuration3).toBe("");
});
});
46 changes: 34 additions & 12 deletions frontend/components/gmm-home/gmm-jobs-list/hooks/use-job-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,20 @@ export type JobStatus =

export interface UseJobCardProps {
uuid: string;
duration: number;
}

export interface UseJobCardReturn {
currentUUID: string | string[] | undefined;
handleClick: () => void;
getCardStyle: (isSelected: boolean) => React.CSSProperties;
formatJobDuration: (duration: number) => string;
formatDurationText: (status: JobStatus, duration?: number) => string;
isProgressOrSuspend: (status: JobStatus) => boolean;
}

/**
* Custom hook for managing JobCard state and interactions
*/
export function useJobCard({
uuid,
duration,
}: UseJobCardProps): UseJobCardReturn {
export function useJobCard({ uuid }: UseJobCardProps): UseJobCardReturn {
const router = useRouter();
const currentUUID = router.query.experiment;

Expand Down Expand Up @@ -60,11 +56,37 @@ export function useJobCard({
/**
* Format duration text
*/
const formatJobDuration = (duration: number): string => {
return (
"Running for " +
formatDuration(intervalToDuration({ start: 0, end: duration * 1000 }))
);
const formatDurationText = (status: JobStatus, duration?: number): string => {
if (status !== "progress" || !duration) {
return "";
}

const durationObj = intervalToDuration({
start: 0,
end: duration,
});

// Convert days to hours and add to existing hours
const totalHours = (durationObj.days || 0) * 24 + (durationObj.hours || 0);

// Create abbreviated format: "xxh xxm xxs"
const parts: string[] = [];

if (totalHours > 0) {
parts.push(`${totalHours}h`);
}

if (durationObj.minutes) {
parts.push(`${durationObj.minutes}m`);
}

if (durationObj.seconds) {
parts.push(`${durationObj.seconds}s`);
}

const str = parts.join(" ");

return `Running for ${str}`;
};

/**
Expand All @@ -78,7 +100,7 @@ export function useJobCard({
currentUUID,
handleClick,
getCardStyle,
formatJobDuration,
formatDurationText,
isProgressOrSuspend,
};
}
7 changes: 4 additions & 3 deletions frontend/components/gmm-home/gmm-jobs-list/job-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ const JobCard: React.FC<Props> = (props) => {
currentUUID,
handleClick,
getCardStyle,
formatJobDuration,
formatDurationText,
isProgressOrSuspend,
} = useJobCard({
uuid,
duration,
});

/**
Expand All @@ -37,7 +36,9 @@ const JobCard: React.FC<Props> = (props) => {
<span className="d-flex flex-column font-monospace">{name}</span>
<div className="d-flex">
{status === "progress" && (
<small className="fw-light">{formatJobDuration(duration)}</small>
<small className="fw-light">
{formatDurationText(status, duration)}
</small>
)}
{status === "success" && (
<Badge pill bg="success" className="align-self-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ const ChildJobCard: React.FC<Props> = (props) => {
: undefined;

const { formatDurationText, handleClick, getCardStyle } = useChildJobCard({
status,
duration,
isSelected,
onClick: props.onClick,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { formatDuration, intervalToDuration } from "date-fns";
import { intervalToDuration } from "date-fns";
import { JobStatus } from "./use-job-card";

export interface UseChildJobCardProps {
status: JobStatus;
duration?: number;
isSelected?: boolean;
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}

Expand All @@ -18,9 +15,6 @@ export interface UseChildJobCardReturn {
* Custom hook for managing ChildJobCard state and interactions
*/
export function useChildJobCard({
status,
duration,
isSelected,
onClick,
}: UseChildJobCardProps): UseChildJobCardReturn {
/**
Expand All @@ -36,7 +30,27 @@ export function useChildJobCard({
end: duration,
});

return `Running for ${formatDuration(durationObj)}`;
// Convert days to hours and add to existing hours
const totalHours = (durationObj.days || 0) * 24 + (durationObj.hours || 0);

// Create abbreviated format: "xxh xxm xxs"
const parts: string[] = [];

if (totalHours > 0) {
parts.push(`${totalHours}h`);
}

if (durationObj.minutes) {
parts.push(`${durationObj.minutes}m`);
}

if (durationObj.seconds) {
parts.push(`${durationObj.seconds}s`);
}

const str = parts.join(" ");

return `Running for ${str}`;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/viewer/model-picker/vae-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const VAEPicker: React.FC<{
</ListGroup>

<RenameModal
defaultName={pickedId}
defaultName={pickedName}
title="Rename VAE model"
label="Please enter a new name for the VAE model."
isOpen={isRenameModelOpen}
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/gmm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ const DetailPane: React.FC = () => {
return (
<div>
<div className="justify-content-between d-flex">
<h3>Experiment: {jobItem.name}</h3>
<h3>{jobItem.name}</h3>
<div>
<Button variant="primary" onClick={refresh}>
<div className="align-items-center d-flex">
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/trainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const ParentPane: React.FC<{
return (
<>
<div className="justify-content-between d-flex">
<h3>Experiment: {item.name}</h3>
<h3>{item.name}</h3>
<div>
<Button
variant="primary"
Expand Down
11 changes: 10 additions & 1 deletion frontend/services/route/train.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const responsePostSearchJobs = z.array(
item_epochs_current: z.number().int().min(0),
}),
])
),
).transform((x) => x.sort((a, b) => a.item_id - b.item_id)),
})
);

Expand All @@ -114,6 +114,15 @@ export const responseGetItem = z.object({
),
epochs_finished: z.array(z.number().int().min(0)),
minimum_NLLs: z.array(z.union([z.null(), z.number().min(0)])),
}).transform((x) => {
const newIndices = Array.from({ length: x.indices.length }, (_, i) => i)
const reorderedIndices = newIndices.map((_, i) => x.indices.indexOf(i))
return {
indices: newIndices,
statuses: x.statuses.map((_, i) => x.statuses[reorderedIndices[i]]),
epochs_finished: x.epochs_finished.map((_, i) => x.epochs_finished[reorderedIndices[i]]),
minimum_NLLs: x.minimum_NLLs.map((_, i) => x.minimum_NLLs[reorderedIndices[i]]),
}
}),
});

Expand Down