Loading src/components/jobs/JobList.tsx +16 −2 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import { GridSizes, getGridSize } from "../../util/gridSizing"; import { FetchNextPageOptions } from "@tanstack/react-query"; import { Outlet, useNavigate } from "@tanstack/react-router"; import { Route as JobsRoute } from "../../routes/simulations.$simulationId.jobs"; import { isBefore } from "date-fns"; function JobListColumn({ size, Loading Loading @@ -44,10 +45,11 @@ export function JobList({ fetchNextPage: (options?: FetchNextPageOptions | undefined) => void; hasNextPage: boolean; isFetchingNextPage: boolean; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; }) { const navigate = useNavigate({ from: JobsRoute.fullPath }); const { simulationId } = JobsRoute.useParams(); const { currentTimestamp } = JobsRoute.useSearch(); const rows: (ColumnHeader[] | Job)[] = [headers, ...jobs]; const parentRef = useRef(null); Loading Loading @@ -84,6 +86,18 @@ export function JobList({ rowVirtualizer.getVirtualItems(), ]); const computeJobState = (job: Job) => { if (job.time_end) { return "Completed"; } if (isBefore(currentTimestamp, job.time_start)) { return "Pending"; } return "Running"; }; return ( <> <Outlet /> Loading Loading @@ -135,7 +149,7 @@ export function JobList({ <JobListColumn size="small">{job.job_id}</JobListColumn> <JobListColumn size="small">{job.name}</JobListColumn> <JobListColumn size="small"> {job.state_current} {computeJobState(job)} </JobListColumn> <JobListColumn size="small">{job.node_count}</JobListColumn> <JobListColumn size="large"> Loading src/components/jobs/list/JobListHeader.tsx +15 −6 Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ function JobListHeaderCell({ size: GridSizes; children: ReactNode; lastIndex: boolean; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; column: ColumnHeader; }) { return ( Loading @@ -21,13 +21,22 @@ function JobListHeaderCell({ className={`${getGridSize(size)} h-full border-neutral-400 dark:border-neutral-900 dark:text-neutral-200 ${lastIndex ? "border-r-0" : "border-r-2"} relative flex items-center justify-center`} onClick={(e) => { e.preventDefault(); onSort(column); if (column.sort.sortable) { const direction = column.sort.sorted ? column.sort.direction === "asc" ? "desc" : "asc" : "asc"; onSort(column.name, column.sort.direction !== "desc", direction); } }} > {children} {column.sort.sortable && ( <ArrowLongDownIcon className={`absolute right-2 h-4 w-4 bg-neutral-300 dark:bg-neutral-700 ${column.sort.sorted && column.sort.direction === "asc" && "rotate-180"} transition-opacity duration-300 ease-in-out group-hover:opacity-100 ${!column.sort.sorted && "opacity-0"}`} className={`absolute right-2 h-4 w-4 bg-neutral-300 dark:bg-neutral-800 ${column.sort?.sorted && column.sort?.direction === "asc" && "rotate-180"} transition-opacity duration-300 ease-in-out group-hover:opacity-100 ${!column.sort?.sorted && "opacity-0"}`} /> )} </button> ); } Loading @@ -35,7 +44,7 @@ function JobListHeaderCell({ interface JobListHeaderProps { headers: ColumnHeader[]; style: CSSProperties; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; } export function JobListHeader({ headers, style, onSort }: JobListHeaderProps) { Loading src/routes/simulations.$simulationId.jobs.$jobId.tsx +9 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,15 @@ import { export const Route = createFileRoute("/simulations/$simulationId/jobs/$jobId")({ component: JobModal, validateSearch: (search: Record<string, unknown>) => { return { start: (search.start as string) || new Date().toISOString(), end: (search.end as string) || new Date().toISOString(), currentTimestamp: search.currentTimestamp as string, playbackInterval: (search.playbackInterval as number) || 15, initialTimestamp: search.initialTimestamp as string, }; }, }); function JobModal() { Loading src/routes/simulations.$simulationId.jobs.tsx +15 −15 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ import { operatorCombinator, sortCombinator } from "../util/filterCombinator"; import { Job } from "../models/Job.model"; import axios from "axios"; import { useState } from "react"; import { ColumnHeader } from "../models/dataGrid/columnHeader.model"; import { Tooltip } from "react-tooltip"; import { JobListFilterModal } from "../components/jobs/list/JobListFilterModal"; Loading @@ -35,6 +34,7 @@ function SimulationJobs() { sortParams + (filterParams ? "&" : "") + filterParams; console.log(sortParams, params); const res = await axios.get<ListResponse<Job>>( `/frontier/simulation/${simulationId}/scheduler/jobs?limit=${jobLimit}&offset=${pageParam * jobLimit}${params}`, ); Loading @@ -49,22 +49,22 @@ function SimulationJobs() { refetchOnWindowFocus: false, }); const onSort = (header: ColumnHeader) => { const newHeaders = [...columns]; const updatedHeader = newHeaders.find( (h) => h.propertyName === header.propertyName, const onSort = ( header: string, sorted: boolean, direction: "asc" | "desc", ) => { const updatedColumns = [...columns]; const currentColumn = updatedColumns.find( (column) => column.name === header, ); if (updatedHeader) { updatedHeader.sort.sorted = !updatedHeader.sort.sorted || updatedHeader.sort.direction === "asc"; updatedHeader.sort.direction = updatedHeader.sort.direction === "desc" ? null : !updatedHeader.sort.direction ? "asc" : "desc"; if (currentColumn) { currentColumn.sort.sorted = sorted; currentColumn.sort.direction = direction; } setColumns(newHeaders); setColumns(updatedColumns); }; if (isLoading) { Loading src/routes/simulations.$simulationId.tsx +2 −2 Original line number Diff line number Diff line import { ArrowLeftIcon, ArrowPathIcon, ChevronDoubleLeftIcon, PauseIcon, PlayIcon, } from "@heroicons/react/24/outline"; Loading Loading @@ -280,7 +280,7 @@ function Simulation() { onClick={onRestart} disabled={differenceInSeconds(currentTimestamp, search.start) === 0} > <ArrowPathIcon <ChevronDoubleLeftIcon className={`h-6 w-6 text-neutral-400`} data-tooltip-id="restart-button" data-tooltip-content="Restart Simulation" Loading Loading
src/components/jobs/JobList.tsx +16 −2 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import { GridSizes, getGridSize } from "../../util/gridSizing"; import { FetchNextPageOptions } from "@tanstack/react-query"; import { Outlet, useNavigate } from "@tanstack/react-router"; import { Route as JobsRoute } from "../../routes/simulations.$simulationId.jobs"; import { isBefore } from "date-fns"; function JobListColumn({ size, Loading Loading @@ -44,10 +45,11 @@ export function JobList({ fetchNextPage: (options?: FetchNextPageOptions | undefined) => void; hasNextPage: boolean; isFetchingNextPage: boolean; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; }) { const navigate = useNavigate({ from: JobsRoute.fullPath }); const { simulationId } = JobsRoute.useParams(); const { currentTimestamp } = JobsRoute.useSearch(); const rows: (ColumnHeader[] | Job)[] = [headers, ...jobs]; const parentRef = useRef(null); Loading Loading @@ -84,6 +86,18 @@ export function JobList({ rowVirtualizer.getVirtualItems(), ]); const computeJobState = (job: Job) => { if (job.time_end) { return "Completed"; } if (isBefore(currentTimestamp, job.time_start)) { return "Pending"; } return "Running"; }; return ( <> <Outlet /> Loading Loading @@ -135,7 +149,7 @@ export function JobList({ <JobListColumn size="small">{job.job_id}</JobListColumn> <JobListColumn size="small">{job.name}</JobListColumn> <JobListColumn size="small"> {job.state_current} {computeJobState(job)} </JobListColumn> <JobListColumn size="small">{job.node_count}</JobListColumn> <JobListColumn size="large"> Loading
src/components/jobs/list/JobListHeader.tsx +15 −6 Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ function JobListHeaderCell({ size: GridSizes; children: ReactNode; lastIndex: boolean; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; column: ColumnHeader; }) { return ( Loading @@ -21,13 +21,22 @@ function JobListHeaderCell({ className={`${getGridSize(size)} h-full border-neutral-400 dark:border-neutral-900 dark:text-neutral-200 ${lastIndex ? "border-r-0" : "border-r-2"} relative flex items-center justify-center`} onClick={(e) => { e.preventDefault(); onSort(column); if (column.sort.sortable) { const direction = column.sort.sorted ? column.sort.direction === "asc" ? "desc" : "asc" : "asc"; onSort(column.name, column.sort.direction !== "desc", direction); } }} > {children} {column.sort.sortable && ( <ArrowLongDownIcon className={`absolute right-2 h-4 w-4 bg-neutral-300 dark:bg-neutral-700 ${column.sort.sorted && column.sort.direction === "asc" && "rotate-180"} transition-opacity duration-300 ease-in-out group-hover:opacity-100 ${!column.sort.sorted && "opacity-0"}`} className={`absolute right-2 h-4 w-4 bg-neutral-300 dark:bg-neutral-800 ${column.sort?.sorted && column.sort?.direction === "asc" && "rotate-180"} transition-opacity duration-300 ease-in-out group-hover:opacity-100 ${!column.sort?.sorted && "opacity-0"}`} /> )} </button> ); } Loading @@ -35,7 +44,7 @@ function JobListHeaderCell({ interface JobListHeaderProps { headers: ColumnHeader[]; style: CSSProperties; onSort: (column: ColumnHeader) => void; onSort: (header: string, sorted: boolean, direction: "asc" | "desc") => void; } export function JobListHeader({ headers, style, onSort }: JobListHeaderProps) { Loading
src/routes/simulations.$simulationId.jobs.$jobId.tsx +9 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,15 @@ import { export const Route = createFileRoute("/simulations/$simulationId/jobs/$jobId")({ component: JobModal, validateSearch: (search: Record<string, unknown>) => { return { start: (search.start as string) || new Date().toISOString(), end: (search.end as string) || new Date().toISOString(), currentTimestamp: search.currentTimestamp as string, playbackInterval: (search.playbackInterval as number) || 15, initialTimestamp: search.initialTimestamp as string, }; }, }); function JobModal() { Loading
src/routes/simulations.$simulationId.jobs.tsx +15 −15 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ import { operatorCombinator, sortCombinator } from "../util/filterCombinator"; import { Job } from "../models/Job.model"; import axios from "axios"; import { useState } from "react"; import { ColumnHeader } from "../models/dataGrid/columnHeader.model"; import { Tooltip } from "react-tooltip"; import { JobListFilterModal } from "../components/jobs/list/JobListFilterModal"; Loading @@ -35,6 +34,7 @@ function SimulationJobs() { sortParams + (filterParams ? "&" : "") + filterParams; console.log(sortParams, params); const res = await axios.get<ListResponse<Job>>( `/frontier/simulation/${simulationId}/scheduler/jobs?limit=${jobLimit}&offset=${pageParam * jobLimit}${params}`, ); Loading @@ -49,22 +49,22 @@ function SimulationJobs() { refetchOnWindowFocus: false, }); const onSort = (header: ColumnHeader) => { const newHeaders = [...columns]; const updatedHeader = newHeaders.find( (h) => h.propertyName === header.propertyName, const onSort = ( header: string, sorted: boolean, direction: "asc" | "desc", ) => { const updatedColumns = [...columns]; const currentColumn = updatedColumns.find( (column) => column.name === header, ); if (updatedHeader) { updatedHeader.sort.sorted = !updatedHeader.sort.sorted || updatedHeader.sort.direction === "asc"; updatedHeader.sort.direction = updatedHeader.sort.direction === "desc" ? null : !updatedHeader.sort.direction ? "asc" : "desc"; if (currentColumn) { currentColumn.sort.sorted = sorted; currentColumn.sort.direction = direction; } setColumns(newHeaders); setColumns(updatedColumns); }; if (isLoading) { Loading
src/routes/simulations.$simulationId.tsx +2 −2 Original line number Diff line number Diff line import { ArrowLeftIcon, ArrowPathIcon, ChevronDoubleLeftIcon, PauseIcon, PlayIcon, } from "@heroicons/react/24/outline"; Loading Loading @@ -280,7 +280,7 @@ function Simulation() { onClick={onRestart} disabled={differenceInSeconds(currentTimestamp, search.start) === 0} > <ArrowPathIcon <ChevronDoubleLeftIcon className={`h-6 w-6 text-neutral-400`} data-tooltip-id="restart-button" data-tooltip-content="Restart Simulation" Loading