From d32a9eb1026797b2ad3d3167a2ad43eec44395de Mon Sep 17 00:00:00 2001 From: theau Date: Wed, 27 Aug 2025 13:54:48 +0200 Subject: [PATCH] feat: avoid re fetching the jobs each time, still in progress --- .../components/JobMonitor/JobDataTable.tsx | 32 +++-- .../src/components/JobMonitor/JobMonitor.tsx | 15 +++ .../components/JobMonitor/JobSearchBar.tsx | 5 + .../components/JobMonitor/jobDataService.ts | 118 ++++++++++-------- .../components/shared/SearchBar/SearchBar.tsx | 8 +- packages/diracx-web/test/e2e/jobMonitor.cy.ts | 5 + 6 files changed, 114 insertions(+), 69 deletions(-) diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx index 9bbaebef..4ad68ed4 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx @@ -67,6 +67,8 @@ interface JobDataTableProps { setColumnPinning: React.Dispatch>; /** Status Colors */ statusColors: Record; + /** Mutate Jobs */ + mutateJobs: () => void; } /** @@ -85,6 +87,7 @@ export function JobDataTable({ columnPinning, setColumnPinning, statusColors, + mutateJobs, }: JobDataTableProps) { // Authentication const { configuration } = useOIDCContext(); @@ -151,10 +154,8 @@ export function JobDataTable({ const selectedIds = Object.keys(rowSelection).map(Number); await deleteJobs(diracxUrl, selectedIds, accessToken, accessTokenPayload); setBackdropOpen(false); - // Refresh the data manually - setSearchBody((prev) => { - return { ...prev }; - }); + // Refresh the data + mutateJobs(); clearSelected(); setSnackbarInfo({ open: true, @@ -182,6 +183,7 @@ export function JobDataTable({ rowSelection, clearSelected, setSearchBody, + mutateJobs, ]); /** @@ -205,10 +207,8 @@ export function JobDataTable({ setBackdropOpen(false); - // Refresh the data manually - setSearchBody((prev) => { - return { ...prev }; - }); + // Refresh the data + mutateJobs(); clearSelected(); // Handle Snackbar Messaging @@ -252,6 +252,7 @@ export function JobDataTable({ rowSelection, clearSelected, setSearchBody, + mutateJobs, ]); /** @@ -273,10 +274,8 @@ export function JobDataTable({ const areSucceedJobs = Object.keys(data.success).length > 0; setBackdropOpen(false); - // Refresh the data manually - setSearchBody((prev) => { - return { ...prev }; - }); + // Refresh the data + mutateJobs(); clearSelected(); // Handle Snackbar Messaging if (areSucceedJobs && failedJobs.length > 0) { @@ -312,7 +311,14 @@ export function JobDataTable({ } finally { setBackdropOpen(false); } - }, [accessToken, diracxUrl, rowSelection, clearSelected, setSearchBody]); + }, [ + accessToken, + diracxUrl, + rowSelection, + clearSelected, + setSearchBody, + mutateJobs, + ]); /** * Handle the history of the selected job diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx index 09dfb55a..bc846cae 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx @@ -28,6 +28,7 @@ import { ColumnDef, } from "@tanstack/react-table"; +import { mutate } from "swr"; import { useApplicationId } from "../../hooks/application"; import { Filter } from "../../types/Filter"; import { @@ -36,9 +37,11 @@ import { CategoryType, JobMonitorChartType, } from "../../types"; +import { useDiracxUrl } from "../../hooks"; import { JobDataTable } from "./JobDataTable"; import { JobSearchBar } from "./JobSearchBar"; import { JobSunburst } from "./JobSunburst"; +import { getSearchJobUrl } from "./jobDataService"; /** * Build the Job Monitor application @@ -48,6 +51,7 @@ import { JobSunburst } from "./JobSunburst"; export default function JobMonitor() { const appId = useApplicationId(); const theme = useTheme(); + const diracxUrl = useDiracxUrl(); // Load the initial state from local storage const initialState = sessionStorage.getItem(`${appId}_State`); @@ -298,6 +302,15 @@ export default function JobMonitor() { })); }, [filters, columns, setSearchBody, setPagination]); + const mutateJobs = useMemo(() => { + return () => { + mutate([ + getSearchJobUrl(diracxUrl, pagination.pageIndex, pagination.pageSize), + searchBody, + ]); + }; + }, [diracxUrl, pagination.pageIndex, pagination.pageSize, searchBody]); + return ( @@ -350,6 +364,7 @@ export default function JobMonitor() { rowSelection={rowSelection} setRowSelection={setRowSelection} statusColors={statusColors} + mutateJobs={mutateJobs} /> )} {chartType === JobMonitorChartType.SUNBURST && ( diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx index 87d805b6..1b6f0028 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobSearchBar.tsx @@ -34,6 +34,8 @@ interface JobSearchBarProps { /** The columns to display in the job monitor */ // eslint-disable-next-line @typescript-eslint/no-explicit-any columns: ColumnDef[]; + /** Function to mutate the job data */ + mutateJobs: () => void; /** Props for the plot type selector */ plotTypeSelectorProps?: { /** The type of the plot */ @@ -51,8 +53,10 @@ export function JobSearchBar({ setFilters, handleApplyFilters, columns, + mutateJobs, plotTypeSelectorProps, }: JobSearchBarProps) { + // Authentication const { configuration } = useOIDCContext(); const { accessToken } = useOidcAccessToken(configuration?.scope); @@ -66,6 +70,7 @@ export function JobSearchBar({ createSuggestions({ diracxUrl, diff --git a/packages/diracx-web-components/src/components/JobMonitor/jobDataService.ts b/packages/diracx-web-components/src/components/JobMonitor/jobDataService.ts index 2c614689..74c08d5f 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/jobDataService.ts +++ b/packages/diracx-web-components/src/components/JobMonitor/jobDataService.ts @@ -1,17 +1,16 @@ "use client"; +import useSWR from "swr"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; -import { useEffect, useState } from "react"; - -dayjs.extend(utc); import { fetcher } from "../../hooks/utils"; import { Filter, SearchBody, Job, JobHistory } from "../../types"; import type { JobSummary } from "../../types"; type TimeUnit = "minute" | "hour" | "day" | "month" | "year"; +dayjs.extend(utc); /** * Convert the 'last' operator in the search body to a date filter. * @param searchBody The search body to be processed @@ -249,64 +248,73 @@ export function useJobs( page: number, rowsPerPage: number, ) { - const [data, setData] = useState(null); - const [headers, setHeaders] = useState(new Headers()); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - if (!diracxUrl) { - setData(null); - setIsLoading(false); - setError(new Error("Invalid URL generated for fetching jobs.")); - return; - } - - let cancelled = false; - - async function fetchJobs() { - setIsLoading(true); + /** The url to fetch jobs */ + const urlGetJobs = getSearchJobUrl(diracxUrl, page, rowsPerPage); - const urlGetJobs = `${diracxUrl}/api/jobs/search?page=${page + 1}&per_page=${rowsPerPage}`; - try { - processSearchBody(searchBody); + /** The key used to revalidate the SWR cache */ + const swrKey: [string, SearchBody] | null = urlGetJobs + ? [urlGetJobs, searchBody] + : null; - const body = { - search: searchBody?.search || [], - sort: searchBody?.sort || [], - }; - - // Expect the response to be an array of objects with all the grouping fields - const res = await fetcher([ - urlGetJobs, - accessToken, - "POST", - body, - ]); - - if (!cancelled) { - setData(res.data); - setIsLoading(false); - setHeaders(res.headers); - setError(null); - } - } catch { - setData(null); - setIsLoading(false); - setError(new Error("Failed to fetch jobs")); + const { + data: swrData, + error: swrError, + isLoading, + } = useSWR( + swrKey, + async ([url, _searchBody]) => { + processSearchBody(_searchBody); + + const body = { + search: _searchBody.search || [], + sort: _searchBody.sort || [], + }; + + if (diracxUrl) { + return await fetcher([url, accessToken, "POST", body]); } - } - fetchJobs(); - return () => { - cancelled = true; - }; - }, [diracxUrl, accessToken, searchBody, page, rowsPerPage]); + return { + headers: new Headers(), + data: null as Job[] | null, + isLoading: false, + error: new Error("Invalid URL generated for fetching jobs."), + }; + }, + { + revalidateOnMount: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + refreshInterval: 0, + shouldRetryOnError: false, + }, + ); return { - headers, - data, + headers: swrData?.headers ?? new Headers(), + data: (swrData?.data as Job[] | undefined) ?? null, isLoading, - error, + error: swrError, }; } + +/** + * Generates the URL for searching jobs. + * + * @param diracxUrl - The base URL of the DiracX API. + * @param page - The page number for pagination. + * @param rowsPerPage - The number of rows per page. + * @returns The URL for the job search API endpoint. + */ +export function getSearchJobUrl( + diracxUrl: string | null, + page: number, + rowsPerPage: number, +) { + if (!diracxUrl) { + return null; + } + + return `${diracxUrl}/api/jobs/search?page=${page + 1}&per_page=${rowsPerPage}`; +} diff --git a/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx b/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx index 6b6a8c8b..7f97a442 100644 --- a/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx +++ b/packages/diracx-web-components/src/components/shared/SearchBar/SearchBar.tsx @@ -56,6 +56,11 @@ export interface SearchBarProps { equations: SearchBarTokenEquation[], setFilters: React.Dispatch>, ) => void; + /** The function to call when the search is refreshed (optional) */ + refreshFunction?: ( + equations: SearchBarTokenEquation[], + setFilters: React.Dispatch>, + ) => void; /** The function to call when the search is cleared (optional) */ clearFunction?: ( setFilters: React.Dispatch>, @@ -85,6 +90,7 @@ export function SearchBar({ createSuggestions, searchFunction = convertAndApplyFilters, clearFunction = defaultClearFunction, + refreshFunction = convertAndApplyFilters, allowKeyWordSearch = true, plotTypeSelectorProps, }: SearchBarProps) { @@ -446,7 +452,7 @@ export function SearchBar({ searchFunction(tokenEquations, setFilters)} + onClick={() => refreshFunction(tokenEquations, setFilters)} disabled={ !tokenEquations.every((eq) => eq.status === EquationStatus.VALID) } diff --git a/packages/diracx-web/test/e2e/jobMonitor.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.cy.ts index 0f79f125..b8c6e37a 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.cy.ts @@ -81,10 +81,15 @@ describe("Job Monitor", () => { ) { cy.log("No data available, adding jobs"); addJobs(55); + // Wait for the jobs to be created + cy.wait(2000); } else { cy.log("Data available, checking if enough jobs are present"); checkAndAddJobs(55); } + + // refresh the jobs + cy.get('[data-testid="RefreshIcon"]').click(); }); });