diff --git a/src/App.tsx b/src/App.tsx index af26dd90..c5a92cd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Drawer from "./components/Drawer"; import Runs from "./pages/Runs"; import Run from "./pages/Run"; import Job from "./pages/Job"; +import Jobs from "./pages/Jobs"; import Queue from "./pages/Queue"; import Nodes from "./pages/Nodes"; import Node from "./pages/Node"; @@ -44,6 +45,7 @@ function App(props: AppProps) { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/Drawer/index.jsx b/src/components/Drawer/index.jsx index 1b971fbd..763e2fd1 100644 --- a/src/components/Drawer/index.jsx +++ b/src/components/Drawer/index.jsx @@ -77,6 +77,9 @@ export default function Drawer(props) { Runs + + Jobs + Nodes diff --git a/src/components/JobHistory/index.jsx b/src/components/JobHistory/index.jsx index 826e1b23..708263fb 100644 --- a/src/components/JobHistory/index.jsx +++ b/src/components/JobHistory/index.jsx @@ -5,19 +5,78 @@ import { Button, Typography, Box } from "@mui/material"; const pageSize = 25; -export default function JobHistory({description}) { - if (!description) { - return null; - } +export default function JobHistory({params}) { - const jobHistoryQuery = useJobHistory(description, pageSize); + + const jobHistoryQuery = useJobHistory(params, pageSize); return ( - - - Past {pageSize} jobs with same description: - + ); } + + + + +// type FilterMenuProps = { +// isOpen: boolean; +// table: MRT_TableInstance; +// }; + + +const FILTER_SECTIONS = ["run", "build", "result"] +const FILTER_SECTIONS_COLUMNS = [ + ["scheduled", "suite", "machine_type", "user"], + ["branch", "sha1"], + ["status"], +] + +// function FilterMenu({ isOpen, table}: FilterMenuProps) { +function FilterMenu({ isOpen, table}) { + if (!isOpen) { + return null; + } + + return ( + + {FILTER_SECTIONS_COLUMNS.map((_, sectionIndex) => ( + + + Filter by {FILTER_SECTIONS[sectionIndex]} details... + + + {table.getLeafHeaders().map((header) => { + if (FILTER_SECTIONS_COLUMNS[sectionIndex].includes(header.id)) { + return ( + + + + ); + } + return null; + })} + + + ))} + + ) +} \ No newline at end of file diff --git a/src/components/JobList/index.tsx b/src/components/JobList/index.tsx index 6102489f..30ef6d1d 100644 --- a/src/components/JobList/index.tsx +++ b/src/components/JobList/index.tsx @@ -1,12 +1,27 @@ -import { ReactNode, useMemo } from "react"; +import { ReactNode, useMemo, useState, SetStateAction } from "react"; +import { parse } from "date-fns"; +import { useDebounce } from "usehooks-ts"; import DescriptionIcon from "@mui/icons-material/Description"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; +import Badge from '@mui/material/Badge'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import Menu from '@mui/material/Menu'; +import type { + DecodedValueMap, + QueryParamConfigMap, + SetQuery, +} from "use-query-params"; import { useMaterialReactTable, MaterialReactTable, + MRT_TableHeadCellFilterContainer, type MRT_ColumnDef, type MRT_Row, + type MRT_Updater, + type MRT_ColumnFiltersState, + type MRT_TableInstance, } from 'material-react-table'; import type { UseQueryResult } from "@tanstack/react-query"; import { type Theme } from "@mui/material/styles"; @@ -14,24 +29,59 @@ import { type Theme } from "@mui/material/styles"; import { formatDate, formatDuration } from "../../lib/utils"; import IconLink from "../../components/IconLink"; import Link from "../../components/Link"; +import TableFilterMenu from "../../components/TableFilterMenu"; import type { Job, JobList, Run } from "../../lib/paddles.d"; -import { dirName } from "../../lib/utils"; +import { dirName, formatDay } from "../../lib/utils"; import useDefaultTableOptions from "../../lib/table"; import sentryIcon from "./assets/sentry.svg"; +const DEFAULT_PAGE_SIZE = 25; +const FILTER_SECTIONS = { + "job": ["user", "description", "tasks", "job_id"], + "machine": ["machine_type", "nodes", "os_type", "os_version"], + "time": ["posted", "started", "updated"], +} + const columns: MRT_ColumnDef[] = [ + { + header: "user", + accessorKey: "user", + maxSize: 30, + }, + { + header: "job ID", + accessorKey: "job_id", + maxSize: 30, + // grow: true, + Cell: ({ row }) => { + return ( + + {row.original.job_id} + + ); + }, + }, + { + header: "priority", + accessorKey: "priority", + maxSize: 20, + enableColumnFilter: false, + }, { header: "status", accessorKey: "status", - size: 120, + maxSize: 40, filterVariant: "select", }, { header: "links", id: "links", - size: 75, + maxSize: 20, Cell: ({ row }) => { const log_url = row.original.log_href; const sentry_url = row.original.sentry_event; @@ -55,21 +105,6 @@ const columns: MRT_ColumnDef[] = [ ); }, }, - { - header: "job ID", - accessorKey: "job_id", - size: 110, - Cell: ({ row }) => { - return ( - - {row.original.job_id} - - ); - }, - }, { header: "tasks", id: "tasks", @@ -89,7 +124,7 @@ const columns: MRT_ColumnDef[] = [ }, { header: "description", - size: 200, + minSize: 200, accessorFn: (row: Job) => row.description + "", filterFn: 'contains', enableColumnFilter: true, @@ -108,7 +143,7 @@ const columns: MRT_ColumnDef[] = [ accessorFn: (row: Job) => formatDate(row.updated), filterVariant: 'date', sortingFn: "datetime", - size: 150, + maxSize: 150, }, { header: "started", @@ -116,12 +151,12 @@ const columns: MRT_ColumnDef[] = [ accessorFn: (row: Job) => formatDate(row.started), filterVariant: 'date', sortingFn: "datetime", - size: 150, + maxSize: 30, }, { header: "runtime", id: "runtime", - size: 110, + maxSize: 30, accessorFn: (row: Job) => { const start = Date.parse(row.started); const end = Date.parse(row.updated); @@ -133,7 +168,7 @@ const columns: MRT_ColumnDef[] = [ { header: "duration", id: "duration", - size: 120, + maxSize: 60, accessorFn: (row: Job) => formatDuration(row.duration), enableColumnFilter: false, @@ -154,17 +189,20 @@ const columns: MRT_ColumnDef[] = [ header: "machine type", accessorKey: "machine_type", filterVariant: "select", + size: 20, }, { header: "OS type", - size: 85, + accessorKey: "os_type", + size: 40, accessorFn: (row: Job) => row.os_type + "", filterVariant: "select", }, { header: "OS version", + accessorKey: "os_version", accessorFn: (row: Job) => row.os_version + "", - size: 85, + size: 50, filterVariant: "select", }, { @@ -173,7 +211,7 @@ const columns: MRT_ColumnDef[] = [ accessorFn: (row: Job) => { return Object.keys(row.targets || row.roles || {}).length || 0; }, - size: 85, + maxSize: 20, }, ]; @@ -219,42 +257,108 @@ function JobDetailPanel(props: JobDetailPanelProps): ReactNode { ) }; +type JobListParams = { + [key: string]: number|string; +} + type JobListProps = { + params: DecodedValueMap; + setter: SetQuery; query: UseQueryResult | UseQueryResult; sortMode?: "time" | "id"; + showColumns?: string[]; } -export default function JobList({ query, sortMode }: JobListProps) { +export default function JobList({ params, setter, query, sortMode, showColumns = [] }: JobListProps) { + const [openFilterMenu, setOpenFilterMenu] = useState(false); + const [dropMenuAnchorEl, setDropMenuAnchor] = useState(null); + const options = useDefaultTableOptions(); const data = useMemo(() => { return (query.data?.jobs || []).filter(item => !! item.id); }, [query, sortMode]); + const debouncedParams = useDebounce(params, 500); + let columnFilters: MRT_ColumnFiltersState = []; + Object.entries(debouncedParams).forEach(param => { + const [id, value] = param; + if ( id === "date" && !!value ) { + columnFilters.push({ + id: "scheduled", + value: parse(value, "yyyy-MM-dd", new Date()) + }) + } else { + columnFilters.push({id, value}) + } + }); + const onColumnFiltersChange = (updater: MRT_Updater) => { + if ( ! ( updater instanceof Function ) ) return; + const result: JobListParams = {pageSize: params.pageSize}; + const updated = updater(columnFilters); + updated.forEach(item => { + if ( ! item.id ) return; + if ( item.value instanceof Date ) { + result.date = formatDay(item.value); + } else if ( typeof item.value === "string" || typeof item.value === "number" ) { + result[item.id] = item.value + } + }); + setter(result); + } + const toggleFilterMenu = (event: { currentTarget: SetStateAction; }) => { + if (dropMenuAnchorEl) { + setDropMenuAnchor(null); + setOpenFilterMenu(false); + } else { + setDropMenuAnchor(event.currentTarget); + setOpenFilterMenu(true); + } + } + + let defaultColumn_: { [key: string]: boolean } = { + posted: false, + updated: false, + duration: false, + waiting: false, + tasks: false, + description: false, + os_version: false, + os_type: false, + user: false, + priority: false, + } + if (showColumns) { + showColumns.map((col: string) => defaultColumn_[col] = true) + } + // const defaultColumn_ = (defaultColumns.map((col) => { return {col: true}})) const table = useMaterialReactTable({ ...options, columns, + layoutMode: 'semantic', data: data, + enableColumnResizing: true, enableFacetedValues: true, enableGlobalFilter: true, enableGlobalFilterRankedResults: false, - positionGlobalFilter: "left", + positionGlobalFilter: "right", globalFilterFn: 'contains', muiSearchTextFieldProps: { placeholder: 'Search across all fields', sx: { minWidth: '200px' }, }, + muiFilterTextFieldProps: ({ column }) => ({ + label: column.columnDef.header, + placeholder: '', + }), + enableColumnActions: false, + manualFiltering: true, + onColumnFiltersChange, + onPaginationChange: setter, initialState: { ...options.initialState, - columnVisibility: { - posted: false, - updated: false, - duration: false, - waiting: false, - tasks: false, - description: false, - }, + columnVisibility: defaultColumn_, pagination: { pageIndex: 0, - pageSize: 25, + pageSize: DEFAULT_PAGE_SIZE, }, sorting: [ { @@ -265,7 +369,12 @@ export default function JobList({ query, sortMode }: JobListProps) { showGlobalFilter: true, }, state: { + columnFilters, isLoading: query.isLoading || query.isFetching, + pagination: { + pageIndex: params.page || 0, + pageSize: params.pageSize || DEFAULT_PAGE_SIZE, + }, }, renderDetailPanel: JobDetailPanel, muiTableBodyRowProps: ({row, isDetailPanel}) => { @@ -276,7 +385,53 @@ export default function JobList({ query, sortMode }: JobListProps) { if ( category ) return { className: category }; return {}; }, + columnFilterDisplayMode: 'custom', + enableColumnFilters: false, + renderTopToolbarCustomActions: ({ table }) => ( + + (filter.value ? count + 1 : count), 0)} + > + + + + ), }); if (query.isError) return null; - return + return ( +
+
+ + { table.getState().columnFilters.map((column) => { + let filterValue = column.value; + if (column.id == "scheduled") { + const parsedDate = new Date(column.value as string); + filterValue = (parsedDate.toISOString().split('T')[0]) + } + + return (column.value ? `${column.id}: '${filterValue}' ` : "") + })} + + + + +
+ +
+ ) } diff --git a/src/components/RunList/index.tsx b/src/components/RunList/index.tsx index c4ace186..abf808d5 100644 --- a/src/components/RunList/index.tsx +++ b/src/components/RunList/index.tsx @@ -26,6 +26,7 @@ import { parse } from "date-fns"; import { useRuns } from "../../lib/paddles"; import { formatDate, formatDay, formatDuration } from "../../lib/utils"; import IconLink from "../../components/IconLink"; +import TableFilterMenu from "../../components/TableFilterMenu"; import type { Run, RunResult, @@ -47,6 +48,11 @@ const NON_FILTER_PARAMS = [ "page", "pageSize", ]; +const FILTER_SECTIONS: { [key: string]: string[] } = { + "run": ["scheduled", "suite", "machine_type", "user"], + "build": ["branch", "sha1"], + "result": ["status"], +} const _columns: MRT_ColumnDef[] = [ { @@ -373,7 +379,7 @@ export default function RunList(props: RunListProps) { 'aria-labelledby': 'filter-button', }} > - + @@ -381,67 +387,3 @@ export default function RunList(props: RunListProps) { ) } - -// FilterMenu - -type FilterMenuProps = { - isOpen: boolean; - table: MRT_TableInstance; -}; - - -const FILTER_SECTIONS = ["run", "build", "result"] -const FILTER_SECTIONS_COLUMNS = [ - ["scheduled", "suite", "machine_type", "user"], - ["branch", "sha1"], - ["status"], -] - -function FilterMenu({ isOpen, table}: FilterMenuProps) { - if (!isOpen) { - return null; - } - - return ( - - {FILTER_SECTIONS_COLUMNS.map((_, sectionIndex) => ( - - - - Filter by {FILTER_SECTIONS[sectionIndex]} details... - - - - {table.getLeafHeaders().map((header) => { - if (FILTER_SECTIONS_COLUMNS[sectionIndex].includes(header.id)) { - return ( - - - - ); - } - return null; - })} - - - ))} - - ) -} diff --git a/src/components/TableFilterMenu/index.tsx b/src/components/TableFilterMenu/index.tsx new file mode 100644 index 00000000..23ed73ac --- /dev/null +++ b/src/components/TableFilterMenu/index.tsx @@ -0,0 +1,69 @@ +import Grid from '@mui/material/Grid'; +import { + MRT_TableHeadCellFilterContainer, + type MRT_TableInstance, +} from 'material-react-table'; + +import type { + Run, + Job, +} from "../../lib/paddles"; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + + +type FilterMenuProps = { + isOpen: boolean; + table: MRT_TableInstance; + filterSections: { [key: string]: string[] }; +}; + + +export default function TableFilterMenu({ isOpen, table, filterSections }: FilterMenuProps) { + if (!isOpen) { + return null; + } + console.log(table.getLeafHeaders()); + console.log(table) + return ( + + {Object.keys(filterSections).map((sectionName, sectionIndex) => ( + + + Filter by {sectionName} details... + + + {table.getFlatHeaders().map((header) => { + if (filterSections[sectionName].includes(header.id)) { + return ( + + + + ); + } + return null; + })} + + + ))} + + ) +} + diff --git a/src/lib/paddles.ts b/src/lib/paddles.ts index d35a6433..51cdb7d8 100644 --- a/src/lib/paddles.ts +++ b/src/lib/paddles.ts @@ -13,7 +13,7 @@ import type { const PADDLES_SERVER = import.meta.env.VITE_PADDLES_SERVER || "https://paddles.front.sepia.ceph.com"; -function getURL(endpoint: string, params?: GetURLParams) { +function getURL(endpoint: string, params?: GetURLParams, onlyQueryParams: boolean = false) { // Because paddles' API is clunky, we have to do extra work. If it were // more inuitive, we could replace everything until the next comment with // just these lines: @@ -28,6 +28,7 @@ function getURL(endpoint: string, params?: GetURLParams) { delete params_[key]; return; } + if (!onlyQueryParams) { switch (key) { case "page": params_[key] = Number(value) + 1; @@ -48,6 +49,7 @@ function getURL(endpoint: string, params?: GetURLParams) { uri += `${key}/${value}/`; delete params_[key]; } + } }); const queryString = new URLSearchParams(params_).toString(); if (queryString) uri += `?${queryString}`; @@ -115,9 +117,9 @@ function useMachineTypes() { }); } -function useJobHistory(description: string, pageSize: number): UseQueryResult { - const url = getURL(`/jobs/`, { 'description': description, "pageSize": pageSize }); - const query = useQuery(["job-history", { url }], { +function useJobs(params: GetURLParams): UseQueryResult { + const url = getURL(`/jobs/`, params, true); + const query = useQuery(["jobs", { url }], { select: (data: Job[]) => { data.forEach((item) => { item.id = item.job_id + ""; @@ -261,5 +263,5 @@ export { useNodes, useStatsNodeLocks, useStatsNodeJobs, - useJobHistory, + useJobs, }; diff --git a/src/pages/Job/index.jsx b/src/pages/Job/index.jsx index 9222f78d..134a2529 100644 --- a/src/pages/Job/index.jsx +++ b/src/pages/Job/index.jsx @@ -5,7 +5,7 @@ import Typography from "@mui/material/Typography"; import Accordion from "@mui/material/Accordion"; import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; -import Button from "@mui/material/Button"; +// import Button from "@mui/material/Button"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ScheduleIcon from "@mui/icons-material/Schedule"; import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline"; @@ -22,8 +22,6 @@ import Link from "../../components/Link"; import CodeBlock from "../../components/CodeBlock"; import { useJob } from "../../lib/paddles"; import { getDuration, dirName } from "../../lib/utils"; -import JobHistory from "../../components/JobHistory"; -import { useState } from "react"; function StatusIcon({ status }) { const theme = useTheme(); @@ -158,7 +156,7 @@ function JobDetails({ query }) { export default function Job() { const { name, job_id } = useParams(); const query = useJob(name, job_id); - const [showJobHistory, toggleShowJobHistory] = useState(false); + return ( @@ -177,14 +175,6 @@ export default function Job() { - - { showJobHistory ? - (query.data?.description ? : null) - :null - } diff --git a/src/pages/Jobs/index.tsx b/src/pages/Jobs/index.tsx new file mode 100644 index 00000000..9e18a032 --- /dev/null +++ b/src/pages/Jobs/index.tsx @@ -0,0 +1,36 @@ +import JobList from "../../components/JobList"; +import { Typography } from "@mui/material"; +import { useQueryParams, NumberParam, StringParam } from "use-query-params"; +import { useJobs } from "../../lib/paddles"; + + +export default function Jobs() { + const [params, setParams] = useQueryParams({ + page: NumberParam, + pageSize: NumberParam, + description: StringParam, + status: StringParam, + sha1: StringParam, + branch: StringParam, + user: StringParam, + posted_after: StringParam, + posted_before: StringParam + }); + + const jobsQuery = useJobs(params); + + return ( +
+ + Jobs + + +
+ ) +} \ No newline at end of file diff --git a/src/pages/Run/index.tsx b/src/pages/Run/index.tsx index 48276697..30c203c8 100644 --- a/src/pages/Run/index.tsx +++ b/src/pages/Run/index.tsx @@ -74,7 +74,7 @@ export default function Run() { - + ); }