From e61426fce1053379a42f39a127c620154886f2f9 Mon Sep 17 00:00:00 2001 From: Jiri Podivin Date: Wed, 25 Mar 2026 14:56:51 +0100 Subject: [PATCH 1/3] Dashboard frontend for Log Detective API in packit We will only be using text field from the Log Detective response. Other fields are not used at the moment, and may in fact be removed in near future. Assisted-by: Claude Opus 4.6 Signed-off-by: Jiri Podivin --- frontend/src/apiDefinitions.ts | 68 ++++++++++ .../logdetective/LogDetectiveGroup.tsx | 128 ++++++++++++++++++ .../logdetective/LogDetectiveResult.tsx | 111 +++++++++++++++ .../logdetective/LogDetectiveResultsTable.tsx | 111 +++++++++++++++ .../queries/logdetective/logDetectiveGroup.ts | 28 ++++ .../logdetective/logDetectiveGroupQuery.ts | 17 +++ .../logdetective/logDetectiveResult.ts | 28 ++++ .../logdetective/logDetectiveResultQuery.ts | 17 +++ .../logdetective/logDetectiveResults.ts | 30 ++++ .../logdetective/logDetectiveResultsQuery.ts | 15 ++ frontend/src/routeTree.gen.ts | 76 +++++++++++ frontend/src/routes/jobs/log-detective.tsx | 12 ++ .../src/routes/jobs_/log-detective.$id.tsx | 15 ++ .../routes/jobs_/log-detective.group.$id.tsx | 15 ++ 14 files changed, 671 insertions(+) create mode 100644 frontend/src/components/logdetective/LogDetectiveGroup.tsx create mode 100644 frontend/src/components/logdetective/LogDetectiveResult.tsx create mode 100644 frontend/src/components/logdetective/LogDetectiveResultsTable.tsx create mode 100644 frontend/src/queries/logdetective/logDetectiveGroup.ts create mode 100644 frontend/src/queries/logdetective/logDetectiveGroupQuery.ts create mode 100644 frontend/src/queries/logdetective/logDetectiveResult.ts create mode 100644 frontend/src/queries/logdetective/logDetectiveResultQuery.ts create mode 100644 frontend/src/queries/logdetective/logDetectiveResults.ts create mode 100644 frontend/src/queries/logdetective/logDetectiveResultsQuery.ts create mode 100644 frontend/src/routes/jobs/log-detective.tsx create mode 100644 frontend/src/routes/jobs_/log-detective.$id.tsx create mode 100644 frontend/src/routes/jobs_/log-detective.group.$id.tsx diff --git a/frontend/src/apiDefinitions.ts b/frontend/src/apiDefinitions.ts index c9dbc19d..612e6c9a 100644 --- a/frontend/src/apiDefinitions.ts +++ b/frontend/src/apiDefinitions.ts @@ -495,3 +495,71 @@ export interface OSHScan { task_id: number | null; url: string | null; } + +// /api/log-detective +export interface LogDetectiveResultGroup { + packit_id: number; + analysis_id: string; + status: string; + chroot: string; + commit_sha: string; + log_detective_response: LogDetectiveResponse | null; + target_build: string | null; + run_ids: number[]; + submitted_time: number | null; +} + +// /api/log-detective/groups/$id +export interface LogDetectiveGroup { + packit_id: number; + submitted_time: number | null; + run_ids: number[]; + log_detective_target_ids: number[]; + pr_id: number | null; + issue_id: number | null; + branch_name: string | null; + release: string | null; + anitya_version: string | null; + anitya_project_id: number | null; + anitya_project_name: string | null; + anitya_package: string | null; + non_git_upstream: boolean; + project_url: string; + repo_name: string; + repo_namespace: string; +} + +// /api/log-detective/$id +export interface LogDetectiveExplanation { + logprobs: unknown | null; + text: string; +} + +export interface LogDetectiveResponse { + explanation: LogDetectiveExplanation; + response_certainty: number; +} + +export interface LogDetectiveResult { + packit_id: number; + analysis_id: string; + branch_name: string | null; + chroot: string; + commit_sha: string; + run_ids: number[]; + status: string; + log_detective_response: LogDetectiveResponse | null; + target_build: string | null; + submitted_time: number | null; + project_url: string; + pr_id: number | null; + issue_id: number | null; + release: string | null; + anitya_version: string | null; + anitya_project_id: number | null; + anitya_project_name: string | null; + anitya_package: string | null; + non_git_upstream: boolean; + repo_name: string; + repo_namespace: string; +} diff --git a/frontend/src/components/logdetective/LogDetectiveGroup.tsx b/frontend/src/components/logdetective/LogDetectiveGroup.tsx new file mode 100644 index 00000000..87ec5a06 --- /dev/null +++ b/frontend/src/components/logdetective/LogDetectiveGroup.tsx @@ -0,0 +1,128 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { + Card, + CardBody, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + List, + ListItem, + PageSection, + Title, +} from "@patternfly/react-core"; + +import { useQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; +import { logDetectiveGroupQueryOptions } from "../../queries/logdetective/logDetectiveGroupQuery"; +import { Route as LogDetectiveGroupRoute } from "../../routes/jobs_/log-detective.group.$id"; +import { ErrorConnection } from "../errors/ErrorConnection"; +import { Preloader } from "../shared/Preloader"; +import { Timestamp } from "../shared/Timestamp"; +import { TriggerLink, TriggerSuffix } from "../trigger/TriggerLink"; + +export const LogDetectiveGroup = () => { + const { id } = LogDetectiveGroupRoute.useParams(); + + const { data, isError, isLoading } = useQuery( + logDetectiveGroupQueryOptions({ id }), + ); + + // If backend API is down + if (isError) { + return ; + } + + if (data && "error" in data) { + return ( + + + + + Not Found. + + + + + ); + } + + return ( + <> + + + Log Detective Group + + {data ? ( + + + + ) : ( + <> + )} + + + + + + {!data ? ( + isLoading || data === undefined ? ( + + ) : ( + + + + + Not Found. + + + + + ) + ) : ( + + + + Submitted Time + + + + + + + Log Detective Targets + + + + {data.log_detective_target_ids.map((targetId) => ( + + + #{targetId} + + + ))} + + + + + Run IDs + + {data.run_ids.join(", ")} + + + + + )} + + + + ); +}; diff --git a/frontend/src/components/logdetective/LogDetectiveResult.tsx b/frontend/src/components/logdetective/LogDetectiveResult.tsx new file mode 100644 index 00000000..3585cf71 --- /dev/null +++ b/frontend/src/components/logdetective/LogDetectiveResult.tsx @@ -0,0 +1,111 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { + Card, + CardBody, + CodeBlock, + CodeBlockCode, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + PageSection, + Title, +} from "@patternfly/react-core"; + +import { useQuery } from "@tanstack/react-query"; +import { logDetectiveResultQueryOptions } from "../../queries/logdetective/logDetectiveResultQuery"; +import { Route as LogDetectiveRoute } from "../../routes/jobs_/log-detective.$id"; +import { ErrorConnection } from "../errors/ErrorConnection"; +import { Preloader } from "../shared/Preloader"; + +export const LogDetectiveResult = () => { + const { id } = LogDetectiveRoute.useParams(); + + const { data, isError, isLoading } = useQuery( + logDetectiveResultQueryOptions({ id }), + ); + + // If backend API is down + if (isError) { + return ; + } + + if (data && "error" in data) { + return ( + + + + + Not Found. + + + + + ); + } + + return ( + <> + + + Log Detective Results + + + + + {!data ? ( + isLoading || data === undefined ? ( + + ) : ( + + + + + Not Found. + + + + + ) + ) : ( + <> + + + + Analysis ID + + {data.analysis_id} + + + + + {data.log_detective_response?.explanation ? ( + + + + Explanation + + + + {data.log_detective_response.explanation.text} + + + + + + + ) : null} + + )} + + + + ); +}; diff --git a/frontend/src/components/logdetective/LogDetectiveResultsTable.tsx b/frontend/src/components/logdetective/LogDetectiveResultsTable.tsx new file mode 100644 index 00000000..962b7c3e --- /dev/null +++ b/frontend/src/components/logdetective/LogDetectiveResultsTable.tsx @@ -0,0 +1,111 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { useState } from "react"; + +import { + Table, + TableVariant, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@patternfly/react-table"; + +import { SkeletonTable } from "@patternfly/react-component-groups"; +import { useQuery } from "@tanstack/react-query"; +import { logDetectiveResultsQueryOptions } from "../../queries/logdetective/logDetectiveResultsQuery"; +import { ErrorConnection } from "../errors/ErrorConnection"; +import { PackitPagination } from "../shared/PackitPagination"; +import { PackitPaginationContext } from "../shared/PackitPaginationContext"; +import { Timestamp } from "../shared/Timestamp"; +import { StatusLabel } from "../statusLabels/StatusLabel"; + +export const LogDetectiveResultsTable = () => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(10); + const value = { page, setPage, perPage, setPerPage }; + + // Headings + const columnNames = { + packit_id: "Packit ID", + analysisId: "Analysis ID", + target: "Target", + commitSha: "Commit SHA", + timeSubmitted: "Time Submitted", + }; + + const { isLoading, isError, data } = useQuery( + logDetectiveResultsQueryOptions(page, perPage), + ); + + const TableHeads = [ + + {columnNames.packit_id} + , + + {columnNames.analysisId} + , + + {columnNames.target} + , + + {columnNames.commitSha} + , + + {columnNames.timeSubmitted} + , + ]; + // If backend API is down + if (isError) { + return ; + } + + return ( + + + {isLoading ? ( + + ) : ( + + + {TableHeads} + + + {data?.map((log_detective_result) => ( + + + + + + + + ))} + +
+ {log_detective_result.packit_id} + + {log_detective_result.analysis_id} + + + + {log_detective_result.commit_sha} + + +
+ )} +
+ ); +}; diff --git a/frontend/src/queries/logdetective/logDetectiveGroup.ts b/frontend/src/queries/logdetective/logDetectiveGroup.ts new file mode 100644 index 00000000..f6e2660c --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveGroup.ts @@ -0,0 +1,28 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { LogDetectiveGroup } from "../../apiDefinitions"; + +export interface fetchLogDetectiveGroupProps { + id: string; + signal?: AbortSignal; +} + +// Fetch data from dashboard backend (or if we want, directly from the API) +export const fetchLogDetectiveGroup = async ({ + id, + signal, +}: fetchLogDetectiveGroupProps): Promise => { + const data = await fetch( + `${import.meta.env.VITE_API_URL}/log-detective/groups/${id}`, + { signal }, + ) + .then((response) => response.json()) + .catch((err) => { + if (err.status === 404) { + throw new Error(`Log Detective group ${id} not found!`); + } + throw err; + }); + return data; +}; diff --git a/frontend/src/queries/logdetective/logDetectiveGroupQuery.ts b/frontend/src/queries/logdetective/logDetectiveGroupQuery.ts new file mode 100644 index 00000000..c55cd147 --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveGroupQuery.ts @@ -0,0 +1,17 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { queryOptions } from "@tanstack/react-query"; +import { + fetchLogDetectiveGroup, + fetchLogDetectiveGroupProps, +} from "./logDetectiveGroup"; + +type QueryParameters = Omit; + +export const logDetectiveGroupQueryOptions = (params: QueryParameters) => + queryOptions({ + queryKey: ["log-detective-group", params], + queryFn: async ({ signal }) => + await fetchLogDetectiveGroup({ signal, ...params }), + }); diff --git a/frontend/src/queries/logdetective/logDetectiveResult.ts b/frontend/src/queries/logdetective/logDetectiveResult.ts new file mode 100644 index 00000000..3adbff29 --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveResult.ts @@ -0,0 +1,28 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { LogDetectiveResult } from "../../apiDefinitions"; + +export interface fetchLogDetectiveResultProps { + id: string; + signal?: AbortSignal; +} + +// Fetch data from dashboard backend (or if we want, directly from the API) +export const fetchLogDetectiveResult = async ({ + id, + signal, +}: fetchLogDetectiveResultProps): Promise => { + const data = await fetch( + `${import.meta.env.VITE_API_URL}/log-detective/${id}`, + { signal }, + ) + .then((response) => response.json()) + .catch((err) => { + if (err.status === 404) { + throw new Error(`Log Detective run ${id} not found!`); + } + throw err; + }); + return data; +}; diff --git a/frontend/src/queries/logdetective/logDetectiveResultQuery.ts b/frontend/src/queries/logdetective/logDetectiveResultQuery.ts new file mode 100644 index 00000000..4903f51b --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveResultQuery.ts @@ -0,0 +1,17 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { queryOptions } from "@tanstack/react-query"; +import { + fetchLogDetectiveResult, + fetchLogDetectiveResultProps, +} from "./logDetectiveResult"; + +type QueryParameters = Omit; + +export const logDetectiveResultQueryOptions = (params: QueryParameters) => + queryOptions({ + queryKey: ["log-detective-results", params], + queryFn: async ({ signal }) => + await fetchLogDetectiveResult({ signal, ...params }), + }); diff --git a/frontend/src/queries/logdetective/logDetectiveResults.ts b/frontend/src/queries/logdetective/logDetectiveResults.ts new file mode 100644 index 00000000..d0ca7bc1 --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveResults.ts @@ -0,0 +1,30 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { LogDetectiveResultGroup } from "../../apiDefinitions"; + +export interface fetchLogDetectiveResultProps { + pageParam: number; + perPage: number; + signal?: AbortSignal; +} + +// Fetch data from dashboard backend (or if we want, directly from the API) +export const fetchLogDetectiveResults = async ({ + pageParam = 1, + perPage, + signal, +}: fetchLogDetectiveResultProps): Promise => { + const data = await fetch( + `${import.meta.env.VITE_API_URL}/log-detective?page=${pageParam}&per_page=${perPage}`, + { signal }, + ) + .then((response) => response.json()) + .catch((err) => { + if (err.status === 404) { + throw new Error(`Log Detective results not found!`); + } + throw err; + }); + return data; +}; diff --git a/frontend/src/queries/logdetective/logDetectiveResultsQuery.ts b/frontend/src/queries/logdetective/logDetectiveResultsQuery.ts new file mode 100644 index 00000000..58a7d4ec --- /dev/null +++ b/frontend/src/queries/logdetective/logDetectiveResultsQuery.ts @@ -0,0 +1,15 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { queryOptions } from "@tanstack/react-query"; +import { fetchLogDetectiveResults } from "./logDetectiveResults"; + +export const logDetectiveResultsQueryOptions = ( + pageParam: number, + perPage: number = 20, +) => + queryOptions({ + queryKey: ["log-detective-results", { pageParam, perPage }], + queryFn: async ({ signal }) => + await fetchLogDetectiveResults({ pageParam, perPage, signal }), + }); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 4bc66c47..41d3adac 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -31,6 +31,7 @@ import { Route as JobsPullFromUpstreamImport } from './routes/jobs/pull-from-ups import { Route as JobsProposeDownstreamsImport } from './routes/jobs/propose-downstreams' import { Route as JobsProposeDownstreamImport } from './routes/jobs/propose-downstream' import { Route as JobsOpenscanhubImport } from './routes/jobs/openscanhub' +import { Route as JobsLogDetectiveImport } from './routes/jobs/log-detective' import { Route as JobsKojiTagRequestsImport } from './routes/jobs/koji-tag-requests' import { Route as JobsKojiDownstreamImport } from './routes/jobs/koji-downstream' import { Route as JobsKojiBuildsImport } from './routes/jobs/koji-builds' @@ -45,12 +46,14 @@ import { Route as JobsSrpmIdImport } from './routes/jobs_/srpm.$id' import { Route as JobsPullFromUpstreamIdImport } from './routes/jobs_/pull-from-upstream.$id' import { Route as JobsProposeDownstreamIdImport } from './routes/jobs_/propose-downstream.$id' import { Route as JobsOpenscanhubIdImport } from './routes/jobs_/openscanhub.$id' +import { Route as JobsLogDetectiveIdImport } from './routes/jobs_/log-detective.$id' import { Route as JobsKojiIdImport } from './routes/jobs_/koji.$id' import { Route as JobsKojiTagRequestIdImport } from './routes/jobs_/koji-tag-request.$id' import { Route as JobsKojiDownstreamIdImport } from './routes/jobs_/koji-downstream.$id' import { Route as JobsCoprIdImport } from './routes/jobs_/copr.$id' import { Route as JobsBodhiIdImport } from './routes/jobs_/bodhi.$id' import { Route as ProjectsForgeNamespaceRepoImport } from './routes/projects/$forge.$namespace.$repo' +import { Route as JobsLogDetectiveGroupIdImport } from './routes/jobs_/log-detective.group.$id' // Create Virtual Routes @@ -160,6 +163,11 @@ const JobsOpenscanhubRoute = JobsOpenscanhubImport.update({ getParentRoute: () => JobsRoute, } as any) +const JobsLogDetectiveRoute = JobsLogDetectiveImport.update({ + path: '/log-detective', + getParentRoute: () => JobsRoute, +} as any) + const JobsKojiTagRequestsRoute = JobsKojiTagRequestsImport.update({ path: '/koji-tag-requests', getParentRoute: () => JobsRoute, @@ -239,6 +247,11 @@ const JobsOpenscanhubIdRoute = JobsOpenscanhubIdImport.update({ getParentRoute: () => rootRoute, } as any) +const JobsLogDetectiveIdRoute = JobsLogDetectiveIdImport.update({ + path: '/jobs/log-detective/$id', + getParentRoute: () => rootRoute, +} as any) + const JobsKojiIdRoute = JobsKojiIdImport.update({ path: '/jobs/koji/$id', getParentRoute: () => rootRoute, @@ -273,6 +286,11 @@ const ProjectsForgeNamespaceRepoRoute = ProjectsForgeNamespaceRepoImport.update( import('./routes/projects/$forge.$namespace.$repo.lazy').then((d) => d.Route), ) +const JobsLogDetectiveGroupIdRoute = JobsLogDetectiveGroupIdImport.update({ + path: '/jobs/log-detective/group/$id', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -382,6 +400,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof JobsKojiTagRequestsImport parentRoute: typeof JobsImport } + '/jobs/log-detective': { + id: '/jobs/log-detective' + path: '/log-detective' + fullPath: '/jobs/log-detective' + preLoaderRoute: typeof JobsLogDetectiveImport + parentRoute: typeof JobsImport + } '/jobs/openscanhub': { id: '/jobs/openscanhub' path: '/openscanhub' @@ -508,6 +533,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof JobsKojiIdImport parentRoute: typeof rootRoute } + '/jobs/log-detective/$id': { + id: '/jobs/log-detective/$id' + path: '/jobs/log-detective/$id' + fullPath: '/jobs/log-detective/$id' + preLoaderRoute: typeof JobsLogDetectiveIdImport + parentRoute: typeof rootRoute + } '/jobs/openscanhub/$id': { id: '/jobs/openscanhub/$id' path: '/jobs/openscanhub/$id' @@ -550,6 +582,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectsForgeNamespaceLazyImport parentRoute: typeof rootRoute } + '/jobs/log-detective/group/$id': { + id: '/jobs/log-detective/group/$id' + path: '/jobs/log-detective/group/$id' + fullPath: '/jobs/log-detective/group/$id' + preLoaderRoute: typeof JobsLogDetectiveGroupIdImport + parentRoute: typeof rootRoute + } '/projects/$forge/$namespace/$repo': { id: '/projects/$forge/$namespace/$repo' path: '/projects/$forge/$namespace/$repo' @@ -572,6 +611,7 @@ interface JobsRouteChildren { JobsKojiBuildsRoute: typeof JobsKojiBuildsRoute JobsKojiDownstreamRoute: typeof JobsKojiDownstreamRoute JobsKojiTagRequestsRoute: typeof JobsKojiTagRequestsRoute + JobsLogDetectiveRoute: typeof JobsLogDetectiveRoute JobsOpenscanhubRoute: typeof JobsOpenscanhubRoute JobsProposeDownstreamRoute: typeof JobsProposeDownstreamRoute JobsProposeDownstreamsRoute: typeof JobsProposeDownstreamsRoute @@ -594,6 +634,7 @@ const JobsRouteChildren: JobsRouteChildren = { JobsKojiBuildsRoute: JobsKojiBuildsRoute, JobsKojiDownstreamRoute: JobsKojiDownstreamRoute, JobsKojiTagRequestsRoute: JobsKojiTagRequestsRoute, + JobsLogDetectiveRoute: JobsLogDetectiveRoute, JobsOpenscanhubRoute: JobsOpenscanhubRoute, JobsProposeDownstreamRoute: JobsProposeDownstreamRoute, JobsProposeDownstreamsRoute: JobsProposeDownstreamsRoute, @@ -624,6 +665,7 @@ export interface FileRoutesByFullPath { '/jobs/koji-builds': typeof JobsKojiBuildsRoute '/jobs/koji-downstream': typeof JobsKojiDownstreamRoute '/jobs/koji-tag-requests': typeof JobsKojiTagRequestsRoute + '/jobs/log-detective': typeof JobsLogDetectiveRoute '/jobs/openscanhub': typeof JobsOpenscanhubRoute '/jobs/propose-downstream': typeof JobsProposeDownstreamRoute '/jobs/propose-downstreams': typeof JobsProposeDownstreamsRoute @@ -642,12 +684,14 @@ export interface FileRoutesByFullPath { '/jobs/koji-downstream/$id': typeof JobsKojiDownstreamIdRoute '/jobs/koji-tag-request/$id': typeof JobsKojiTagRequestIdRoute '/jobs/koji/$id': typeof JobsKojiIdRoute + '/jobs/log-detective/$id': typeof JobsLogDetectiveIdRoute '/jobs/openscanhub/$id': typeof JobsOpenscanhubIdRoute '/jobs/propose-downstream/$id': typeof JobsProposeDownstreamIdRoute '/jobs/pull-from-upstream/$id': typeof JobsPullFromUpstreamIdRoute '/jobs/srpm/$id': typeof JobsSrpmIdRoute '/jobs/testing-farm/$id': typeof JobsTestingFarmIdRoute '/projects/$forge/$namespace': typeof ProjectsForgeNamespaceLazyRoute + '/jobs/log-detective/group/$id': typeof JobsLogDetectiveGroupIdRoute '/projects/$forge/$namespace/$repo': typeof ProjectsForgeNamespaceRepoRoute } @@ -666,6 +710,7 @@ export interface FileRoutesByTo { '/jobs/koji-builds': typeof JobsKojiBuildsRoute '/jobs/koji-downstream': typeof JobsKojiDownstreamRoute '/jobs/koji-tag-requests': typeof JobsKojiTagRequestsRoute + '/jobs/log-detective': typeof JobsLogDetectiveRoute '/jobs/openscanhub': typeof JobsOpenscanhubRoute '/jobs/propose-downstream': typeof JobsProposeDownstreamRoute '/jobs/propose-downstreams': typeof JobsProposeDownstreamsRoute @@ -684,12 +729,14 @@ export interface FileRoutesByTo { '/jobs/koji-downstream/$id': typeof JobsKojiDownstreamIdRoute '/jobs/koji-tag-request/$id': typeof JobsKojiTagRequestIdRoute '/jobs/koji/$id': typeof JobsKojiIdRoute + '/jobs/log-detective/$id': typeof JobsLogDetectiveIdRoute '/jobs/openscanhub/$id': typeof JobsOpenscanhubIdRoute '/jobs/propose-downstream/$id': typeof JobsProposeDownstreamIdRoute '/jobs/pull-from-upstream/$id': typeof JobsPullFromUpstreamIdRoute '/jobs/srpm/$id': typeof JobsSrpmIdRoute '/jobs/testing-farm/$id': typeof JobsTestingFarmIdRoute '/projects/$forge/$namespace': typeof ProjectsForgeNamespaceLazyRoute + '/jobs/log-detective/group/$id': typeof JobsLogDetectiveGroupIdRoute '/projects/$forge/$namespace/$repo': typeof ProjectsForgeNamespaceRepoRoute } @@ -710,6 +757,7 @@ export interface FileRoutesById { '/jobs/koji-builds': typeof JobsKojiBuildsRoute '/jobs/koji-downstream': typeof JobsKojiDownstreamRoute '/jobs/koji-tag-requests': typeof JobsKojiTagRequestsRoute + '/jobs/log-detective': typeof JobsLogDetectiveRoute '/jobs/openscanhub': typeof JobsOpenscanhubRoute '/jobs/propose-downstream': typeof JobsProposeDownstreamRoute '/jobs/propose-downstreams': typeof JobsProposeDownstreamsRoute @@ -728,12 +776,14 @@ export interface FileRoutesById { '/jobs/koji-downstream/$id': typeof JobsKojiDownstreamIdRoute '/jobs/koji-tag-request/$id': typeof JobsKojiTagRequestIdRoute '/jobs/koji/$id': typeof JobsKojiIdRoute + '/jobs/log-detective/$id': typeof JobsLogDetectiveIdRoute '/jobs/openscanhub/$id': typeof JobsOpenscanhubIdRoute '/jobs/propose-downstream/$id': typeof JobsProposeDownstreamIdRoute '/jobs/pull-from-upstream/$id': typeof JobsPullFromUpstreamIdRoute '/jobs/srpm/$id': typeof JobsSrpmIdRoute '/jobs/testing-farm/$id': typeof JobsTestingFarmIdRoute '/projects/$forge/$namespace': typeof ProjectsForgeNamespaceLazyRoute + '/jobs/log-detective/group/$id': typeof JobsLogDetectiveGroupIdRoute '/projects/$forge/$namespace/$repo': typeof ProjectsForgeNamespaceRepoRoute } @@ -755,6 +805,7 @@ export interface FileRouteTypes { | '/jobs/koji-builds' | '/jobs/koji-downstream' | '/jobs/koji-tag-requests' + | '/jobs/log-detective' | '/jobs/openscanhub' | '/jobs/propose-downstream' | '/jobs/propose-downstreams' @@ -773,12 +824,14 @@ export interface FileRouteTypes { | '/jobs/koji-downstream/$id' | '/jobs/koji-tag-request/$id' | '/jobs/koji/$id' + | '/jobs/log-detective/$id' | '/jobs/openscanhub/$id' | '/jobs/propose-downstream/$id' | '/jobs/pull-from-upstream/$id' | '/jobs/srpm/$id' | '/jobs/testing-farm/$id' | '/projects/$forge/$namespace' + | '/jobs/log-detective/group/$id' | '/projects/$forge/$namespace/$repo' fileRoutesByTo: FileRoutesByTo to: @@ -796,6 +849,7 @@ export interface FileRouteTypes { | '/jobs/koji-builds' | '/jobs/koji-downstream' | '/jobs/koji-tag-requests' + | '/jobs/log-detective' | '/jobs/openscanhub' | '/jobs/propose-downstream' | '/jobs/propose-downstreams' @@ -814,12 +868,14 @@ export interface FileRouteTypes { | '/jobs/koji-downstream/$id' | '/jobs/koji-tag-request/$id' | '/jobs/koji/$id' + | '/jobs/log-detective/$id' | '/jobs/openscanhub/$id' | '/jobs/propose-downstream/$id' | '/jobs/pull-from-upstream/$id' | '/jobs/srpm/$id' | '/jobs/testing-farm/$id' | '/projects/$forge/$namespace' + | '/jobs/log-detective/group/$id' | '/projects/$forge/$namespace/$repo' id: | '__root__' @@ -838,6 +894,7 @@ export interface FileRouteTypes { | '/jobs/koji-builds' | '/jobs/koji-downstream' | '/jobs/koji-tag-requests' + | '/jobs/log-detective' | '/jobs/openscanhub' | '/jobs/propose-downstream' | '/jobs/propose-downstreams' @@ -856,12 +913,14 @@ export interface FileRouteTypes { | '/jobs/koji-downstream/$id' | '/jobs/koji-tag-request/$id' | '/jobs/koji/$id' + | '/jobs/log-detective/$id' | '/jobs/openscanhub/$id' | '/jobs/propose-downstream/$id' | '/jobs/pull-from-upstream/$id' | '/jobs/srpm/$id' | '/jobs/testing-farm/$id' | '/projects/$forge/$namespace' + | '/jobs/log-detective/group/$id' | '/projects/$forge/$namespace/$repo' fileRoutesById: FileRoutesById } @@ -881,12 +940,14 @@ export interface RootRouteChildren { JobsKojiDownstreamIdRoute: typeof JobsKojiDownstreamIdRoute JobsKojiTagRequestIdRoute: typeof JobsKojiTagRequestIdRoute JobsKojiIdRoute: typeof JobsKojiIdRoute + JobsLogDetectiveIdRoute: typeof JobsLogDetectiveIdRoute JobsOpenscanhubIdRoute: typeof JobsOpenscanhubIdRoute JobsProposeDownstreamIdRoute: typeof JobsProposeDownstreamIdRoute JobsPullFromUpstreamIdRoute: typeof JobsPullFromUpstreamIdRoute JobsSrpmIdRoute: typeof JobsSrpmIdRoute JobsTestingFarmIdRoute: typeof JobsTestingFarmIdRoute ProjectsForgeNamespaceLazyRoute: typeof ProjectsForgeNamespaceLazyRoute + JobsLogDetectiveGroupIdRoute: typeof JobsLogDetectiveGroupIdRoute ProjectsForgeNamespaceRepoRoute: typeof ProjectsForgeNamespaceRepoRoute } @@ -905,12 +966,14 @@ const rootRouteChildren: RootRouteChildren = { JobsKojiDownstreamIdRoute: JobsKojiDownstreamIdRoute, JobsKojiTagRequestIdRoute: JobsKojiTagRequestIdRoute, JobsKojiIdRoute: JobsKojiIdRoute, + JobsLogDetectiveIdRoute: JobsLogDetectiveIdRoute, JobsOpenscanhubIdRoute: JobsOpenscanhubIdRoute, JobsProposeDownstreamIdRoute: JobsProposeDownstreamIdRoute, JobsPullFromUpstreamIdRoute: JobsPullFromUpstreamIdRoute, JobsSrpmIdRoute: JobsSrpmIdRoute, JobsTestingFarmIdRoute: JobsTestingFarmIdRoute, ProjectsForgeNamespaceLazyRoute: ProjectsForgeNamespaceLazyRoute, + JobsLogDetectiveGroupIdRoute: JobsLogDetectiveGroupIdRoute, ProjectsForgeNamespaceRepoRoute: ProjectsForgeNamespaceRepoRoute, } @@ -940,12 +1003,14 @@ export const routeTree = rootRoute "/jobs/koji-downstream/$id", "/jobs/koji-tag-request/$id", "/jobs/koji/$id", + "/jobs/log-detective/$id", "/jobs/openscanhub/$id", "/jobs/propose-downstream/$id", "/jobs/pull-from-upstream/$id", "/jobs/srpm/$id", "/jobs/testing-farm/$id", "/projects/$forge/$namespace", + "/jobs/log-detective/group/$id", "/projects/$forge/$namespace/$repo" ] }, @@ -964,6 +1029,7 @@ export const routeTree = rootRoute "/jobs/koji-builds", "/jobs/koji-downstream", "/jobs/koji-tag-requests", + "/jobs/log-detective", "/jobs/openscanhub", "/jobs/propose-downstream", "/jobs/propose-downstreams", @@ -1024,6 +1090,10 @@ export const routeTree = rootRoute "filePath": "jobs/koji-tag-requests.tsx", "parent": "/jobs" }, + "/jobs/log-detective": { + "filePath": "jobs/log-detective.tsx", + "parent": "/jobs" + }, "/jobs/openscanhub": { "filePath": "jobs/openscanhub.tsx", "parent": "/jobs" @@ -1088,6 +1158,9 @@ export const routeTree = rootRoute "/jobs/koji/$id": { "filePath": "jobs_/koji.$id.tsx" }, + "/jobs/log-detective/$id": { + "filePath": "jobs_/log-detective.$id.tsx" + }, "/jobs/openscanhub/$id": { "filePath": "jobs_/openscanhub.$id.tsx" }, @@ -1106,6 +1179,9 @@ export const routeTree = rootRoute "/projects/$forge/$namespace": { "filePath": "projects/$forge.$namespace_.lazy.tsx" }, + "/jobs/log-detective/group/$id": { + "filePath": "jobs_/log-detective.group.$id.tsx" + }, "/projects/$forge/$namespace/$repo": { "filePath": "projects/$forge.$namespace.$repo.tsx" } diff --git a/frontend/src/routes/jobs/log-detective.tsx b/frontend/src/routes/jobs/log-detective.tsx new file mode 100644 index 00000000..c01fd5ea --- /dev/null +++ b/frontend/src/routes/jobs/log-detective.tsx @@ -0,0 +1,12 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { createFileRoute } from "@tanstack/react-router"; +import { LogDetectiveResultsTable } from "../../components/logdetective/LogDetectiveResultsTable"; + +export const Route = createFileRoute("/jobs/log-detective")({ + staticData: { + title: "Log Detective results", + }, + component: () => LogDetectiveResultsTable(), +}); diff --git a/frontend/src/routes/jobs_/log-detective.$id.tsx b/frontend/src/routes/jobs_/log-detective.$id.tsx new file mode 100644 index 00000000..85a8e86a --- /dev/null +++ b/frontend/src/routes/jobs_/log-detective.$id.tsx @@ -0,0 +1,15 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { createFileRoute } from "@tanstack/react-router"; +import { LogDetectiveResult } from "../../components/logdetective/LogDetectiveResult"; +import { logDetectiveResultQueryOptions } from "../../queries/logdetective/logDetectiveResultQuery"; + +export const Route = createFileRoute("/jobs/log-detective/$id")({ + staticData: { + title: "Log Detective result detail", + }, + loader: ({ context: { queryClient }, params: { id } }) => + queryClient.ensureQueryData(logDetectiveResultQueryOptions({ id })), + component: LogDetectiveResult, +}); diff --git a/frontend/src/routes/jobs_/log-detective.group.$id.tsx b/frontend/src/routes/jobs_/log-detective.group.$id.tsx new file mode 100644 index 00000000..1ad13dc9 --- /dev/null +++ b/frontend/src/routes/jobs_/log-detective.group.$id.tsx @@ -0,0 +1,15 @@ +// Copyright Contributors to the Packit project. +// SPDX-License-Identifier: MIT + +import { createFileRoute } from "@tanstack/react-router"; +import { LogDetectiveGroup } from "../../components/logdetective/LogDetectiveGroup"; +import { logDetectiveGroupQueryOptions } from "../../queries/logdetective/logDetectiveGroupQuery"; + +export const Route = createFileRoute("/jobs/log-detective/group/$id")({ + staticData: { + title: "Log Detective group detail", + }, + loader: ({ context: { queryClient }, params: { id } }) => + queryClient.ensureQueryData(logDetectiveGroupQueryOptions({ id })), + component: LogDetectiveGroup, +}); From 29205c56dc9693c4ea28cd868154a34e67762629 Mon Sep 17 00:00:00 2001 From: Jiri Podivin Date: Thu, 26 Mar 2026 14:53:09 +0100 Subject: [PATCH 2/3] Putting Log Detective in the job list and expanding information of runs Signed-off-by: Jiri Podivin --- frontend/src/components/jobs/Jobs.tsx | 3 +++ .../logdetective/LogDetectiveResult.tsx | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/frontend/src/components/jobs/Jobs.tsx b/frontend/src/components/jobs/Jobs.tsx index 46525a8a..5e94cb30 100644 --- a/frontend/src/components/jobs/Jobs.tsx +++ b/frontend/src/components/jobs/Jobs.tsx @@ -69,6 +69,9 @@ const Jobs = () => { Koji Tagging Requests + + Log Detective + diff --git a/frontend/src/components/logdetective/LogDetectiveResult.tsx b/frontend/src/components/logdetective/LogDetectiveResult.tsx index 3585cf71..f45fc9df 100644 --- a/frontend/src/components/logdetective/LogDetectiveResult.tsx +++ b/frontend/src/components/logdetective/LogDetectiveResult.tsx @@ -20,6 +20,8 @@ import { logDetectiveResultQueryOptions } from "../../queries/logdetective/logDe import { Route as LogDetectiveRoute } from "../../routes/jobs_/log-detective.$id"; import { ErrorConnection } from "../errors/ErrorConnection"; import { Preloader } from "../shared/Preloader"; +import { Timestamp } from "../shared/Timestamp"; +import { StatusLabel } from "../statusLabels/StatusLabel"; export const LogDetectiveResult = () => { const { id } = LogDetectiveRoute.useParams(); @@ -76,14 +78,33 @@ export const LogDetectiveResult = () => { + + Packit ID + + {data.packit_id} + + Analysis ID {data.analysis_id} + + Status + + + + + + Submitted Time + + + + {data.log_detective_response?.explanation ? ( From 271b415a9a6cf55a7950863fc149801736a5a0fa Mon Sep 17 00:00:00 2001 From: Jiri Podivin Date: Thu, 26 Mar 2026 15:18:34 +0100 Subject: [PATCH 3/3] Setting color of 'complete' Log Detective analysis to blue Signed-off-by: Jiri Podivin --- frontend/src/components/statusLabels/StatusLabel.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/statusLabels/StatusLabel.tsx b/frontend/src/components/statusLabels/StatusLabel.tsx index 6462491d..44bf2d19 100644 --- a/frontend/src/components/statusLabels/StatusLabel.tsx +++ b/frontend/src/components/statusLabels/StatusLabel.tsx @@ -56,6 +56,10 @@ export const StatusLabel: React.FC = (props) => { setColor("grey"); setIcon(); break; + case "complete": + setColor("blue"); + setIcon(); + break; case "unknown": setColor("grey"); setIcon();