Skip to content

Commit

Permalink
add load more to job list, limit param to API query
Browse files Browse the repository at this point in the history
  • Loading branch information
bgentry committed May 9, 2024
1 parent 990912f commit 3f3123e
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 34 deletions.
22 changes: 21 additions & 1 deletion api_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ func (a *apiHandler) JobList(rw http.ResponseWriter, req *http.Request) {
return
}

params := river.NewJobListParams().First(20)
limit, err := limitFromReq(req, 20)
if err != nil {
http.Error(rw, fmt.Sprintf("invalid limit: %s", err), http.StatusBadRequest)
return
}
params := river.NewJobListParams().First(int(limit))

Check failure on line 171 in api_handler.go

View workflow job for this annotation

GitHub Actions / Go lint

unnecessary conversion (unconvert)

state := rivertype.JobState(req.Form.Get("state"))
switch state {
Expand Down Expand Up @@ -403,6 +408,21 @@ func (a *apiHandler) WorkflowGet(rw http.ResponseWriter, req *http.Request) {
}
}

func limitFromReq(req *http.Request, defaultLimit int) (int, error) {
limitString := req.Form.Get("limit")
if limitString == "" {
return defaultLimit, nil
}
limit, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
return 0, err
}
if limit > 1000 {
return 1000, nil
}
return int(limit), nil
}

type RiverJob struct {
ID int64 `json:"id"`
Args json.RawMessage `json:"args"`
Expand Down
1 change: 1 addition & 0 deletions ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dist
!dist/.gitkeep
dist-ssr
*.local
.eslintcache

# Editor directories and files
.vscode/*
Expand Down
35 changes: 34 additions & 1 deletion ui/src/components/JobList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,24 @@ const JobListItem = ({ job }: JobListItemProps) => (
);

type JobListProps = {
canShowFewer: boolean;
canShowMore: boolean;
loading?: boolean;
jobs: Job[];
showFewer: () => void;
showMore: () => void;
statesAndCounts: StatesAndCounts | undefined;
};

const JobList = ({ jobs, loading, statesAndCounts }: JobListProps) => {
const JobList = ({
canShowFewer,
canShowMore,
jobs,
loading,
showFewer,
showMore,
statesAndCounts,
}: JobListProps) => {
return (
<div className="h-full lg:pl-72">
<TopNav>
Expand Down Expand Up @@ -170,6 +182,27 @@ const JobList = ({ jobs, loading, statesAndCounts }: JobListProps) => {
<JobListItem key={job.id.toString()} job={job} />
))}
</ul>
<nav
className="flex items-center justify-center border-t border-black/5 py-3 dark:border-white/5"
aria-label="Pagination"
>
<button
className={classNames(
"relative inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-slate-900 dark:text-slate-100 ring-1 ring-inset ring-slate-300 dark:ring-slate-700 hover:bg-slate-50 hover:dark:bg-slate-800 focus-visible:outline-offset-0"
)}
disabled={!canShowFewer}
onClick={() => showFewer()}
>
Fewer
</button>
<button
className="relative ml-3 inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-slate-900 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 focus-visible:outline-offset-0 dark:text-slate-100 dark:ring-slate-700 hover:dark:bg-slate-800"
disabled={!canShowMore}
onClick={() => showMore()}
>
More
</button>
</nav>
</div>
)}
</div>
Expand Down
99 changes: 70 additions & 29 deletions ui/src/routes/jobs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";
import {
queryOptions,
useQuery,
useSuspenseQuery,
} from "@tanstack/react-query";

import { JobState } from "@services/types";
import JobList from "@components/JobList";
Expand All @@ -9,59 +13,96 @@ import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
import { createFileRoute } from "@tanstack/react-router";
import { countsByState, countsByStateKey } from "@services/states";

const minimumLimit = 20;
const maximumLimit = 200;

const jobSearchSchema = z.object({
limit: z
.number()
.int()
.min(minimumLimit)
.max(maximumLimit)
.default(minimumLimit)
.catch(minimumLimit),
state: z.nativeEnum(JobState).catch(JobState.Running).optional(),
});

export const Route = createFileRoute("/jobs/")({
validateSearch: jobSearchSchema,
beforeLoad: ({ abortController, search }) => {
return {
jobsQueryOptions: {
queryKey: listJobsKey(search),
queryFn: listJobs,
refetchInterval: 2000,
signal: abortController.signal,
},
statesQueryOptions: {
queryKey: countsByStateKey(),
queryFn: countsByState,
refetchInterval: 2000,
signal: abortController.signal,
},
};
},
loader: async ({
context: { queryClient, jobsQueryOptions, statesQueryOptions },
}) => {
loaderDeps: ({ search: { limit, state } }) => ({ limit, state }),
loader: async ({ context: { queryClient }, deps: { limit, state } }) => {
// TODO: how to pass abortController.signal into ensureQueryData or queryOptions?
// signal: abortController.signal,
await Promise.all([
queryClient.ensureQueryData(jobsQueryOptions),
queryClient.ensureQueryData(statesQueryOptions),
queryClient.ensureQueryData({ ...jobsQueryOptions({ limit, state }) }),
queryClient.ensureQueryData(statesQueryOptions()),
]);
},

component: JobsIndexComponent,
});

function JobsIndexComponent() {
const { jobsQueryOptions, statesQueryOptions } = Route.useRouteContext();
const navigate = Route.useNavigate();
const { limit } = Route.useSearch();
const refreshSettings = useRefreshSetting();
const refetchInterval = !refreshSettings.disabled
? refreshSettings.intervalMs
: 0;

const jobsQuery = useQuery(
Object.assign(jobsQueryOptions, { refetchInterval })
);
const statesQuery = useQuery(
Object.assign(statesQueryOptions, { refetchInterval })
const jobsQuery = useSuspenseQuery(
jobsQueryOptions(Route.useLoaderDeps(), { refetchInterval })
);
const statesQuery = useQuery(statesQueryOptions({ refetchInterval }));

const canShowFewer = limit > minimumLimit;
const canShowMore = limit < maximumLimit;

const showFewer = () => {
navigate({
replace: true,
search: (old) => ({ ...old, limit: Math.max(limit - 20, minimumLimit) }),
});
};
const showMore = () => {
navigate({
replace: true,
search: (old) => ({ ...old, limit: Math.min(limit + 20, maximumLimit) }),
});
};

return (
<JobList
loading={jobsQuery.isLoading || !jobsQuery.data}
canShowFewer={canShowFewer}
canShowMore={canShowMore}
loading={jobsQuery.isLoading}
jobs={jobsQuery.data || []}
showFewer={showFewer}
showMore={showMore}
statesAndCounts={statesQuery.data}
/>
);
}

const jobsQueryOptions = (
{
limit,
state,
}: {
limit?: number;
state?: JobState;
},
opts?: { refetchInterval: number }
) =>
queryOptions({
queryKey: listJobsKey({ limit, state }),
queryFn: listJobs,
refetchInterval: opts?.refetchInterval,
});

const statesQueryOptions = (opts?: { refetchInterval: number }) =>
queryOptions({
queryKey: countsByStateKey(),
queryFn: countsByState,
refetchInterval: opts?.refetchInterval,
});
10 changes: 7 additions & 3 deletions ui/src/services/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export type Job = {
| undefined
? Date
: JobFromAPI[Key] extends AttemptErrorFromAPI[]
? AttemptError[]
: JobFromAPI[Key];
? AttemptError[]
: JobFromAPI[Key];
};

export type JobWithKnownMetadata = Job & {
Expand Down Expand Up @@ -107,6 +107,7 @@ export const cancelJobs: MutationFunction<void, CancelPayload> = async ({
};

type ListJobsFilters = {
limit?: number;
state?: JobState;
};

Expand All @@ -121,7 +122,10 @@ export const listJobs: QueryFunction<Job[], ListJobsKey> = async ({
signal,
}) => {
const [, searchParams] = queryKey;
const query = new URLSearchParams(searchParams);
const searchParamsStringValues = Object.fromEntries(
Object.entries(searchParams).map(([k, v]) => [k, String(v)])
);
const query = new URLSearchParams(searchParamsStringValues);

return API.get<JobFromAPI[]>({ path: "/jobs", query }, { signal }).then(
// Map from JobFromAPI to Job:
Expand Down

0 comments on commit 3f3123e

Please sign in to comment.