- Discarded jobs retained in the dead letter queue. Select a job to
- inspect its failures and retry it.
-
-
- {filterError && (
-
- {filterError}
-
- )}
+
+
+
+
+
+ {alert && (
+
+ {alert}
+
+ )}
+
{loading ? (
-
Loading…
- ) : jobs.length === 0 ? (
-
- {initialFilters.length
- ? "No dead letter jobs match these filters."
- : "No dead letter jobs."}
+
+ Loading…
+ ) : jobs.length === 0 ? (
+
+ {initialFilters.length ? (
+
+ ) : (
+
+ Discarded jobs appear here when the dead letter queue is
+ enabled with Config.DeadLetter.Enabled in River
+ Pro.
+ >
+ }
+ title="No dead letter jobs."
+ />
+ )}
+
) : (
<>
-
-
-
-
-
- Job
-
-
- Queue
-
-
- Attempts
-
-
- Discarded
-
-
-
-
- {jobs.map((job) => (
-
-
-
- {job.kind}{" "}
- #{job.id.toString()}
-
-
-
{job.queue}
-
- {job.attempt} / {job.maxAttempts}
-
-
- {job.finalizedAt ? (
-
- ) : (
- "–"
- )}
-
-
- ))}
-
-
-
-
+ {jobs.map((job) => (
+
+ ))}
+
+
>
)}
@@ -146,13 +161,15 @@ async function fetchSuggestionsForDeadLetters(
type: FilterTypeId,
query: string,
selectedValues: string[],
+ signal?: AbortSignal,
): Promise {
if (type === FilterTypeId.KIND || type === FilterTypeId.QUEUE) {
return fetchDeadLetterSuggestions(
type === FilterTypeId.KIND ? "job_kind" : "queue_name",
query,
selectedValues,
+ signal,
);
}
- return fetchSuggestions(type, query, selectedValues);
+ return fetchSuggestions(type, query, selectedValues, signal);
}
diff --git a/src/components/JobDetail.tsx b/src/components/JobDetail.tsx
index 3040348e..ce26a29e 100644
--- a/src/components/JobDetail.tsx
+++ b/src/components/JobDetail.tsx
@@ -1,6 +1,7 @@
import { Badge } from "@components/Badge";
import ButtonForGroup from "@components/ButtonForGroup";
import ConfirmationDialog from "@components/ConfirmationDialog";
+import CopyableHeadingText from "@components/CopyableHeadingText";
import JobAttempts from "@components/JobAttempts";
import JobTimeline from "@components/JobTimeline";
import JSONTextView from "@components/JSONTextView";
@@ -17,7 +18,7 @@ import { Job, JobWithKnownMetadata } from "@services/jobs";
import { JobState } from "@services/types";
import { Link } from "@tanstack/react-router";
import { capitalize } from "@utils/string";
-import { FormEvent, useState } from "react";
+import { FormEvent, ReactNode, useState } from "react";
type JobDetailProps = {
cancel: () => void;
@@ -26,12 +27,44 @@ type JobDetailProps = {
retry: () => void;
};
+type JobDetailViewProps = {
+ actions: ReactNode;
+ job: Job;
+ // Shown under the heading, for context that applies to the whole job.
+ notice?: ReactNode;
+ title: string;
+};
+
export default function JobDetail({
cancel,
deleteFn,
job,
retry,
}: JobDetailProps) {
+ return (
+
+ }
+ job={job}
+ title="Job Details"
+ />
+ );
+}
+
+// JobDetailView is the job detail page without its actions, so that other
+// panes showing a job (like dead letters) share one layout.
+export function JobDetailView({
+ actions,
+ job,
+ notice,
+ title,
+}: JobDetailViewProps) {
const featureEnabledWorkflows = useFeature("ENABLE_WORKFLOWS", true);
let jobWithMetadata: JobWithKnownMetadata | undefined;
@@ -41,28 +74,26 @@ export default function JobDetail({
return (
<>
-
+
{/* Heading */}
-
-
-
- {job.kind}
-
+
+
+
ID: {job.id.toString()}
+ {notice && (
+
+ {notice}
+
+ )}
-
+ {actions}
diff --git a/src/components/JobList.tsx b/src/components/JobList.tsx
index d8a347fd..024ef1da 100644
--- a/src/components/JobList.tsx
+++ b/src/components/JobList.tsx
@@ -1,11 +1,12 @@
import { Badge } from "@components/Badge";
-import { Button } from "@components/Button";
import ButtonForGroup from "@components/ButtonForGroup";
import ConfirmationDialog from "@components/ConfirmationDialog";
import { CustomCheckbox } from "@components/CustomCheckbox";
import { Dropdown, DropdownItem, DropdownMenu } from "@components/Dropdown";
import { Filter, JobSearch } from "@components/job-search/JobSearch";
import { JobStateFilters } from "@components/JobStateFilters";
+import ListEmptyState from "@components/ListEmptyState";
+import ListPagination from "@components/ListPagination";
import RelativeTimeFormatter from "@components/RelativeTimeFormatter";
import TopNav from "@components/TopNav";
import { useFeatures } from "@contexts/Features.hook";
@@ -152,35 +153,6 @@ const JobListItem = ({
);
};
-type EmptySetIconProps = React.ComponentProps<"svg">;
-
-const EmptySetIcon = (props: EmptySetIconProps) => (
-
-);
-
-const EmptyState = () => (
-
-
-
- No jobs found
-
-
- Try adjusting your filters to find what you're looking for.
-