Skip to content
Open
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
11 changes: 10 additions & 1 deletion handler_api_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (a *autocompleteListEndpoint[TTx]) Execute(ctx context.Context, req *autoco
return listResponseFrom(queuePtrs), nil

default:
return nil, apierror.NewBadRequestf("Invalid facet %q. Valid facets are: job_kind, queue_name", req.Facet)
return nil, apierror.NewBadRequestf("Invalid facet %q. Valid facets are: job_kind, job_tag, queue_name", req.Facet)
}
})
}
Expand Down Expand Up @@ -418,6 +418,7 @@ type jobListRequest struct {
Priorities []int16 `json:"-" validate:"omitempty,min=0,max=10"` // from ExtractRaw
Queues []string `json:"-" validate:"omitempty,max=100"` // from ExtractRaw
State *rivertype.JobState `json:"-" validate:"omitempty,oneof=available cancelled completed discarded pending retryable running scheduled"` // from ExtractRaw
Tags []string `json:"-" validate:"omitempty,max=100"` // from ExtractRaw
}

func (req *jobListRequest) ExtractRaw(r *http.Request) error {
Expand Down Expand Up @@ -462,6 +463,10 @@ func (req *jobListRequest) ExtractRaw(r *http.Request) error {
req.Queues = queues
}

if tags := r.URL.Query()["tags"]; len(tags) > 0 {
req.Tags = tags
}

return nil
}

Expand All @@ -487,6 +492,10 @@ func (a *jobListEndpoint[TTx]) Execute(ctx context.Context, req *jobListRequest)
params = params.Queues(req.Queues...)
}

if len(req.Tags) > 0 {
params = params.Where("ARRAY(SELECT lower(t) FROM unnest(tags) t) && ARRAY(SELECT lower(v) FROM unnest(@tags::varchar[]) v)", river.NamedArgs{"tags": req.Tags})
}

if req.State == nil {
params = params.States(rivertype.JobStateRunning).OrderBy(river.JobListOrderByTime, river.SortOrderAsc)
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/components/job-search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum JobFilterTypeID {
KIND = "kind",
PRIORITY = "priority",
QUEUE = "queue",
TAGS = "tags",
}

export interface FilterType {
Expand Down Expand Up @@ -39,4 +40,9 @@ export const AVAILABLE_FILTERS: FilterType[] = [
label: "queue",
match: "queue:",
},
{
id: JobFilterTypeID.TAGS,
label: "tags",
match: "tags:",
},
];
4 changes: 4 additions & 0 deletions src/routes/jobs/index.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ export const jobSearchSchema = z.object({
.optional()
.transform((v) => (Array.isArray(v) ? v : v ? [v] : undefined)),
state: z.nativeEnum(JobState).default(defaultValues.state),
tags: z
.union([z.string(), z.array(z.string().min(1))])
.optional()
.transform((v) => (Array.isArray(v) ? v : v ? [v] : undefined)),
});
38 changes: 30 additions & 8 deletions src/routes/jobs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,31 @@ export const Route = createFileRoute("/jobs/")({
search: {
middlewares: [
stripSearchParams(defaultValues),
retainSearchParams(["id", "kind", "limit", "priority", "queue"]),
retainSearchParams(["id", "kind", "limit", "priority", "queue", "tags"]),
],
},
beforeLoad: async ({ context }) => {
// No need to check for search.state since it has a default value now
return context;
},
loaderDeps: ({ search: { limit, state, kind, queue, priority, id } }) => {
loaderDeps: ({ search: { limit, state, kind, queue, priority, id, tags } }) => {
return {
kind,
limit: limit || defaultValues.limit,
priority: priority?.map((p) => parseInt(p, 10)),
queue,
state,
id,
tags,
};
},
loader: async ({
context: { queryClient },
deps: { limit, state, kind, queue, id },
deps: { limit, state, kind, queue, id, tags },
}) => {
await Promise.all([
queryClient.ensureQueryData({
...jobsQueryOptions({ limit, state, kind, queue, id }),
...jobsQueryOptions({ limit, state, kind, queue, id, tags }),
}),
queryClient.ensureQueryData(statesQueryOptions()),
]);
Expand All @@ -80,7 +81,7 @@ export const Route = createFileRoute("/jobs/")({

function JobsIndexComponent() {
const navigate = Route.useNavigate();
const { id, limit, state, kind, queue, priority } = Route.useLoaderDeps();
const { id, limit, state, kind, queue, priority, tags } = Route.useLoaderDeps();
const refreshSettings = useRefreshSetting();
const refetchInterval = refreshSettings.intervalMs;
const [pauseRefetches, setJobRefetchesPaused] = useState(false);
Expand All @@ -95,6 +96,7 @@ function JobsIndexComponent() {
kind,
queue,
priority,
tags,
},
{
pauseRefetches,
Expand Down Expand Up @@ -147,6 +149,7 @@ function JobsIndexComponent() {
priority: undefined,
queue: undefined,
id: undefined,
tags: undefined,
};

// Only set values for filters that exist and have values
Expand All @@ -170,6 +173,11 @@ function JobsIndexComponent() {
? filter.values
: undefined;
break;
case FilterTypeId.TAGS:
searchParams.tags = filter.values.length
? filter.values
: undefined;
break;
}
});

Expand All @@ -178,6 +186,7 @@ function JobsIndexComponent() {
kind,
priority: priority?.map(String),
queue,
tags,
};

// Avoid no-op navigations that can race with route transitions.
Expand All @@ -188,7 +197,8 @@ function JobsIndexComponent() {
currentSearchParams.priority,
searchParams.priority,
) &&
areStringArraysEqual(currentSearchParams.queue, searchParams.queue)
areStringArraysEqual(currentSearchParams.queue, searchParams.queue) &&
areStringArraysEqual(currentSearchParams.tags, searchParams.tags)
) {
return;
}
Expand All @@ -204,10 +214,11 @@ function JobsIndexComponent() {
priority?: string[];
queue?: string[];
state: JobState;
tags?: string[];
},
});
},
[id, kind, navigate, priority, queue],
[id, kind, navigate, priority, queue, tags],
);

// Convert current search params to initial filters
Expand Down Expand Up @@ -245,8 +256,16 @@ function JobsIndexComponent() {
values: queue,
});
}
if (tags?.length) {
filters.push({
id: "tags-filter",
match: "tags:",
typeId: FilterTypeId.TAGS,
values: tags,
});
}
return filters;
}, [id, kind, priority, queue]);
}, [id, kind, priority, queue, tags]);

const cancelMutation = useMutation({
mutationFn: async (jobIDs: bigint[], context) =>
Expand Down Expand Up @@ -331,13 +350,15 @@ const jobsQueryOptions = (
kind,
queue,
priority,
tags,
}: {
id?: bigint[];
kind?: string[];
limit: number;
priority?: number[];
queue?: string[];
state: JobState;
tags?: string[];
},
opts?: { pauseRefetches: boolean; refetchInterval: number },
) => {
Expand All @@ -360,6 +381,7 @@ const jobsQueryOptions = (
queues: queue,
priorities: priority,
ids: id,
tags,
}),
queryFn: listJobs,
placeholderData: keepPreviousDataUnlessStateChanged,
Expand Down
6 changes: 5 additions & 1 deletion src/services/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export type ListJobsKey = [
priorities: number[] | undefined;
queues: string[] | undefined;
state: JobState | undefined;
tags: string[] | undefined;
},
];

Expand All @@ -196,6 +197,7 @@ type ListJobsFilters = {
priorities?: number[];
queues?: string[];
state?: JobState;
tags?: string[];
};

export const listJobsKey = (args: ListJobsFilters): ListJobsKey => {
Expand All @@ -208,6 +210,7 @@ export const listJobsKey = (args: ListJobsFilters): ListJobsKey => {
priorities: args.priorities,
queues: args.queues,
state: args.state,
tags: args.tags,
},
];
};
Expand All @@ -216,7 +219,7 @@ export const listJobs: QueryFunction<JobMinimal[], ListJobsKey> = async ({
queryKey,
signal,
}) => {
const [, { ids, kinds, limit, priorities, queues, state }] = queryKey;
const [, { ids, kinds, limit, priorities, queues, state, tags }] = queryKey;

// Build query params object with only defined values
const params: Record<string, string | string[]> = {
Expand All @@ -227,6 +230,7 @@ export const listJobs: QueryFunction<JobMinimal[], ListJobsKey> = async ({
if (priorities?.length) params.priorities = priorities.map(String);
if (queues?.length) params.queues = queues;
if (state) params.state = state;
if (tags?.length) params.tags = tags;

// Convert to URLSearchParams, handling arrays correctly
const query = new URLSearchParams();
Expand Down