diff --git a/ui/src/components/JobAttemptErrors.tsx b/ui/src/components/JobAttemptErrors.tsx
new file mode 100644
index 0000000..65f63a1
--- /dev/null
+++ b/ui/src/components/JobAttemptErrors.tsx
@@ -0,0 +1,73 @@
+import { Job } from "@services/jobs";
+import RelativeTimeFormatter from "./RelativeTimeFormatter";
+import { useState } from "react";
+
+type JobAttemptErrorsProps = {
+ job: Job;
+};
+
+const defaultErrorDisplayCount = 5;
+
+export default function JobAttemptErrors({ job }: JobAttemptErrorsProps) {
+ const [showAllErrors, setShowAllErrors] = useState(false);
+ const errorsToDisplay = showAllErrors
+ ? job.errors.slice().reverse()
+ : job.errors.slice(-1 * defaultErrorDisplayCount).reverse();
+
+ return (
+
+
+
+ Errors
+
+
+ {job.errors.length === 0 ? (
+ <>No errors>
+ ) : (
+ <>
+
+ {errorsToDisplay.map((error) => (
+ -
+
+
+ {error.attempt.toString()}
+
+
+
+ {error.error}
+
+ {error.trace && (
+
+ {error.trace}
+
+ )}
+
+
+
+
+
+
+ ))}
+
+ {job.errors.length > defaultErrorDisplayCount && (
+ <>
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+
+ );
+}
diff --git a/ui/src/components/JobDetail.tsx b/ui/src/components/JobDetail.tsx
index 363caa8..1ba55e4 100644
--- a/ui/src/components/JobDetail.tsx
+++ b/ui/src/components/JobDetail.tsx
@@ -9,10 +9,11 @@ import { Heroicon, JobState } from "@services/types";
import { capitalize } from "@utils/string";
import clsx from "clsx";
import JobTimeline from "./JobTimeline";
-import { FormEvent } from "react";
+import { FormEvent, useMemo, useState } from "react";
import { Link } from "@tanstack/react-router";
import TopNavTitleOnly from "./TopNavTitleOnly";
import RelativeTimeFormatter from "./RelativeTimeFormatter";
+import JobAttemptErrors from "./JobAttemptErrors";
type JobDetailProps = {
cancel: () => void;
@@ -94,6 +95,14 @@ function ActionButtons({
}
export default function JobDetail({ cancel, job, retry }: JobDetailProps) {
+ const [showAllAttempts, setShowAllAttempts] = useState(false);
+ const attemptsToDisplay = useMemo(() => {
+ if (showAllAttempts) {
+ return job.attemptedBy.slice().reverse();
+ }
+ return job.attemptedBy.slice(-5).reverse();
+ }, [job.attemptedBy, showAllAttempts]);
+
return (
<>
@@ -118,7 +127,7 @@ export default function JobDetail({ cancel, job, retry }: JobDetailProps) {
-
+
{/* Description list */}
@@ -218,13 +227,39 @@ export default function JobDetail({ cancel, job, retry }: JobDetailProps) {
Attempted By
-
- {job.attemptedBy.map((attemptedBy, i) => (
-
- {attemptedBy}
-
- ))}
+
+ {attemptsToDisplay.map((attemptedBy, i) => (
+ -
+ {attemptedBy}
+
+ ))}
+
+ {!showAllAttempts && job.attemptedBy.length > 5 && (
+
+ )}
+ {showAllAttempts && (
+
+ )}
+
+
diff --git a/ui/src/components/RefreshPauser.tsx b/ui/src/components/RefreshPauser.tsx
index 929687c..d570cdb 100644
--- a/ui/src/components/RefreshPauser.tsx
+++ b/ui/src/components/RefreshPauser.tsx
@@ -19,6 +19,7 @@ type RefreshIntervalSetting = {
const refreshIntervals: RefreshIntervalSetting[] = [
{ name: "Pause", value: 0 },
+ { name: "1s", value: 1000 },
{ name: "2s", value: 2000 },
{ name: "5s", value: 5000 },
{ name: "10s", value: 10000 },
@@ -62,10 +63,6 @@ export function RefreshPauser(
setDisabled(!disabled)}
- // title={disabled ? "Resume live updates" : "Pause live updates"}
>
{disabled ? "Resume live updates" : "Pause live updates"}
diff --git a/ui/src/services/jobs.ts b/ui/src/services/jobs.ts
index 10862e6..f46a810 100644
--- a/ui/src/services/jobs.ts
+++ b/ui/src/services/jobs.ts
@@ -11,8 +11,8 @@ import { API } from "@utils/api";
// except with keys as snake_case instead of camelCase.
type AttemptErrorFromAPI = {
at: string;
+ attempt: number;
error: string;
- num: number;
trace: string;
};
@@ -87,9 +87,9 @@ const apiAttemptErrorsToAttemptErrors = (
errors: AttemptErrorFromAPI[]
): AttemptError[] => {
return errors.map((error) => ({
+ attempt: error.attempt,
at: new Date(error.at),
error: error.error,
- num: error.num,
trace: error.trace,
}));
};
diff --git a/ui/src/test/factories/job.ts b/ui/src/test/factories/job.ts
index 6df32aa..8a13013 100644
--- a/ui/src/test/factories/job.ts
+++ b/ui/src/test/factories/job.ts
@@ -8,11 +8,11 @@ import { add, sub } from "date-fns";
class AttemptErrorFactory extends Factory {}
export const attemptErrorFactory = AttemptErrorFactory.define(({ params }) => {
- const num = params.num || 1;
+ const attempt = params.attempt || 1;
return {
- at: params.at || sub(new Date(), { seconds: (21 - num) * 7.3 }),
+ at: params.at || sub(new Date(), { seconds: (21 - attempt) * 7.3 }),
+ attempt,
error: "Failed yet again with some Go message",
- num,
trace: "...",
};
});
@@ -69,7 +69,7 @@ class JobFactory extends Factory {
at: add(finalizedAt, {
seconds: faker.number.float({ min: 0.01, max: 2.5 }),
}),
- num: 1,
+ attempt: 1,
}),
],
createdAt,
@@ -106,20 +106,20 @@ class JobFactory extends Factory {
"worker-3",
],
errors: [
- attemptErrorFactory.build({ num: 1 }),
- attemptErrorFactory.build({ num: 2 }),
- attemptErrorFactory.build({ num: 3 }),
- attemptErrorFactory.build({ num: 4 }),
- attemptErrorFactory.build({ num: 5 }),
- attemptErrorFactory.build({ num: 6 }),
- attemptErrorFactory.build({ num: 7 }),
- attemptErrorFactory.build({ num: 8 }),
- attemptErrorFactory.build({ num: 9 }),
+ attemptErrorFactory.build({ attempt: 1 }),
+ attemptErrorFactory.build({ attempt: 2 }),
+ attemptErrorFactory.build({ attempt: 3 }),
+ attemptErrorFactory.build({ attempt: 4 }),
+ attemptErrorFactory.build({ attempt: 5 }),
+ attemptErrorFactory.build({ attempt: 6 }),
+ attemptErrorFactory.build({ attempt: 7 }),
+ attemptErrorFactory.build({ attempt: 8 }),
+ attemptErrorFactory.build({ attempt: 9 }),
attemptErrorFactory.build({
at: add(attemptedAt, {
seconds: faker.number.float({ min: 0.01, max: 95 }),
}),
- num: 10,
+ attempt: 10,
}),
],
createdAt: sub(attemptedAt, { minutes: 31, seconds: 30 }),