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
4 changes: 2 additions & 2 deletions src/components/JobList.stories.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";

import { JobState } from "@services/types";
import { jobFactory } from "@test/factories/job";
import { jobMinimalFactory } from "@test/factories/job";

import JobList from "./JobList";

Expand All @@ -16,7 +16,7 @@ type Story = StoryObj<typeof JobList>;

export const Running: Story = {
args: {
jobs: jobFactory.running().buildList(10),
jobs: jobMinimalFactory.running().buildList(10),
setJobRefetchesPaused: () => {},
state: JobState.Running,
},
Expand Down
12 changes: 6 additions & 6 deletions src/components/JobList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "@heroicons/react/24/outline";
import { useSelected } from "@hooks/use-selected";
import { useShiftSelected } from "@hooks/use-shift-selected";
import { Job } from "@services/jobs";
import { JobMinimal } from "@services/jobs";
import { StatesAndCounts } from "@services/states";
import { JobState } from "@services/types";
import { Link } from "@tanstack/react-router";
Expand All @@ -39,7 +39,7 @@ const states: { [key in JobState]: string } = {
[JobState.Scheduled]: "text-rose-400 bg-rose-400/10",
};

const timestampForRelativeDisplay = (job: Job): Date => {
const timestampForRelativeDisplay = (job: JobMinimal): Date => {
switch (job.state) {
case JobState.Completed:
return job.finalizedAt ? job.finalizedAt : new Date();
Expand All @@ -50,7 +50,7 @@ const timestampForRelativeDisplay = (job: Job): Date => {
}
};

const JobTimeDisplay = ({ job }: { job: Job }): React.JSX.Element => {
const JobTimeDisplay = ({ job }: { job: JobMinimal }): React.JSX.Element => {
return (
<span>
<RelativeTimeFormatter
Expand All @@ -64,7 +64,7 @@ const JobTimeDisplay = ({ job }: { job: Job }): React.JSX.Element => {

type JobListItemProps = {
checked: boolean;
job: Job;
job: JobMinimal;
onChangeSelect: (
checked: boolean,
event: React.ChangeEvent<HTMLInputElement>,
Expand Down Expand Up @@ -159,7 +159,7 @@ export type JobRowsProps = {
canShowMore: boolean;
deleteJobs: (jobIDs: bigint[]) => void;
initialFilters?: Filter[];
jobs: Job[];
jobs: JobMinimal[];
onFiltersChange?: (filters: Filter[]) => void;
retryJobs: (jobIDs: bigint[]) => void;
setJobRefetchesPaused: (value: boolean) => void;
Expand All @@ -174,7 +174,7 @@ type JobListProps = {
canShowMore: boolean;
deleteJobs: (jobIDs: bigint[]) => void;
initialFilters?: Filter[];
jobs: Job[];
jobs: JobMinimal[];
loading?: boolean;
onFiltersChange?: (filters: Filter[]) => void;
retryJobs: (jobIDs: bigint[]) => void;
Expand Down
6 changes: 3 additions & 3 deletions src/routes/jobs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { defaultValues, jobSearchSchema } from "@routes/jobs/index.schema";
import {
cancelJobs,
deleteJobs,
Job,
JobMinimal,
listJobs,
ListJobsKey,
listJobsKey,
Expand Down Expand Up @@ -312,9 +312,9 @@ const jobsQueryOptions = (
opts?: { pauseRefetches: boolean; refetchInterval: number },
) => {
const keepPreviousDataUnlessStateChanged: PlaceholderDataFunction<
Job[],
JobMinimal[],
Error,
Job[],
JobMinimal[],
ListJobsKey
> = (previousData, previousQuery) => {
if (!previousQuery) return undefined;
Expand Down
58 changes: 37 additions & 21 deletions src/services/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,52 @@ export type Job = {
: JobFromAPI[Key] extends AttemptErrorFromAPI[]
? AttemptError[]
: JobFromAPI[Key];
} & {
};

export type JobFromAPI = {
errors: AttemptErrorFromAPI[];
logs: JobLogs;
metadata: KnownMetadata | object;
} & JobMinimalFromAPI;

export type JobLogEntry = {
attempt: number;
log: string;
};

// New type for better organized logs
export type JobLogs = {
[attempt: number]: string;
};

export type JobMinimal = {
[Key in keyof JobMinimalFromAPI as SnakeToCamelCase<Key>]: Key extends
| StringEndingWithUnderscoreAt
| undefined
? Date
: JobMinimalFromAPI[Key];
};

// Represents a Job as received from the API. This just like Job, except with
// string dates instead of Date objects and keys as snake_case instead of
// camelCase.
export type JobFromAPI = {
export type JobMinimalFromAPI = {
args: object;
attempt: number;
attempted_at?: string;
attempted_by: string[];
created_at: string;
errors: AttemptErrorFromAPI[];
finalized_at?: string;
id: bigint;
kind: string;
max_attempts: number;
metadata: KnownMetadata | object;
priority: number;
queue: string;
scheduled_at: string;
state: JobState;
tags: string[];
};

export type JobLogEntry = {
attempt: number;
log: string;
};

// New type for better organized logs
export type JobLogs = {
[attempt: number]: string;
};

export type JobWithKnownMetadata = {
metadata: KnownMetadata;
} & Omit<Job, "metadata">;
Expand Down Expand Up @@ -87,26 +97,32 @@ type RiverJobLogEntry = {
log: string;
};

export const apiJobToJob = (job: JobFromAPI): Job => ({
export const apiJobMinimalToJobMinimal = (
job: JobMinimalFromAPI,
): JobMinimal => ({
args: job.args,
attempt: job.attempt,
attemptedAt: job.attempted_at ? new Date(job.attempted_at) : undefined,
attemptedBy: job.attempted_by,
createdAt: new Date(job.created_at),
errors: apiAttemptErrorsToAttemptErrors(job.errors),
finalizedAt: job.finalized_at ? new Date(job.finalized_at) : undefined,
id: BigInt(job.id),
kind: job.kind,
logs: extractJobLogs(job.metadata),
maxAttempts: job.max_attempts,
metadata: job.metadata,
priority: job.priority,
queue: job.queue,
scheduledAt: new Date(job.scheduled_at),
state: job.state,
tags: job.tags,
});

export const apiJobToJob = (job: JobFromAPI): Job => ({
...apiJobMinimalToJobMinimal(job),
errors: apiAttemptErrorsToAttemptErrors(job.errors),
logs: extractJobLogs(job.metadata),
metadata: job.metadata,
});

const apiAttemptErrorsToAttemptErrors = (
errors: AttemptErrorFromAPI[],
): AttemptError[] => {
Expand Down Expand Up @@ -196,7 +212,7 @@ export const listJobsKey = (args: ListJobsFilters): ListJobsKey => {
];
};

export const listJobs: QueryFunction<Job[], ListJobsKey> = async ({
export const listJobs: QueryFunction<JobMinimal[], ListJobsKey> = async ({
queryKey,
signal,
}) => {
Expand All @@ -222,13 +238,13 @@ export const listJobs: QueryFunction<Job[], ListJobsKey> = async ({
}
});

return API.get<ListResponse<JobFromAPI>>(
return API.get<ListResponse<JobMinimalFromAPI>>(
{ path: "/jobs", query },
{ signal },
).then(
// Map from JobFromAPI to Job:
// TODO: there must be a cleaner way to do this given the type definitions?
(response) => response.data.map(apiJobToJob),
(response) => response.data.map(apiJobMinimalToJobMinimal),
);
};

Expand Down
48 changes: 47 additions & 1 deletion src/test/factories/job.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker";
import { AttemptError, Job } from "@services/jobs";
import { AttemptError, Job, JobMinimal } from "@services/jobs";
import { JobState } from "@services/types";
import { add, sub } from "date-fns";
import { Factory } from "fishery";
Expand Down Expand Up @@ -28,6 +28,52 @@ export const attemptErrorFactory = AttemptErrorFactory.define(({ params }) => {
};
});

// Helper type to extract only JobMinimal fields from Job:
type JobMinimalFields = Pick<Job, keyof JobMinimal>;

class JobMinimalFactory extends Factory<JobMinimal, object> {
available() {
return this.params(jobFactory.available().build());
}

cancelled() {
return this.params(jobFactory.cancelled().build());
}

completed() {
return this.params(jobFactory.completed().build());
}

discarded() {
return this.params(jobFactory.discarded().build());
}

pending() {
return this.params(jobFactory.pending().build());
}

retryable() {
return this.params(jobFactory.retryable().build());
}

running() {
return this.params(jobFactory.running().build());
}

scheduled() {
return this.params(jobFactory.scheduled().build());
}

scheduledSnoozed() {
return this.params(jobFactory.scheduledSnoozed().build());
}
}

export const jobMinimalFactory = JobMinimalFactory.define(({ sequence }) => {
const job = jobFactory.build({ id: BigInt(sequence) });
return job as JobMinimalFields;
});

class JobFactory extends Factory<Job, object> {
available() {
return this.params({});
Expand Down
Loading