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) => ( +
  1. +
    +

    + {error.attempt.toString()} +

    +
    +
    + {error.error} +
    + {error.trace && ( +
    +                            {error.trace}
    +                          
    + )} +

    + +

    +
    +
    +
  2. + ))} +
+ {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 }),