From 2e9618e485077edfc5b0a39dc2d5e0867e93940e Mon Sep 17 00:00:00 2001 From: aldbr Date: Wed, 14 May 2025 10:08:45 +0200 Subject: [PATCH] feat: use FilterToolbar from JobMonitor instead of from DataTable --- .../components/JobMonitor/JobDataTable.tsx | 257 +++------------ .../src/components/JobMonitor/JobMonitor.tsx | 304 +++++++++++++++++- .../src/components/shared/DataTable.tsx | 184 ++--------- .../src/components/shared/FilterForm.tsx | 20 +- .../src/components/shared/FilterToolbar.tsx | 28 +- .../src/types/DashboardItem.ts | 2 - .../diracx-web-components/src/types/Filter.ts | 1 + .../test/FilterForm.test.tsx | 43 +-- .../test/FilterToolbar.test.tsx | 30 +- .../test/JobDataTable.test.tsx | 145 ++++++--- .../diracx-web/src/app/(dashboard)/page.tsx | 2 +- .../extensions/src/app/(dashboard)/page.tsx | 2 +- 12 files changed, 535 insertions(+), 483 deletions(-) diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx index d7cd511f..b720ecda 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx @@ -1,20 +1,7 @@ "use client"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import Box from "@mui/material/Box"; -import { - blue, - orange, - grey, - green, - red, - lightBlue, - purple, - teal, - blueGrey, - lime, - amber, -} from "@mui/material/colors"; import { Alert, AlertColor, @@ -23,25 +10,22 @@ import { Backdrop, CircularProgress, Snackbar, - lighten, - darken, - useTheme, } from "@mui/material"; import { useOidcAccessToken } from "@axa-fr/react-oidc"; import { Delete, Clear, Replay } from "@mui/icons-material"; import { - createColumnHelper, - ColumnPinningState, - RowSelectionState, useReactTable, getCoreRowModel, + ColumnDef, + ColumnPinningState, + RowSelectionState, VisibilityState, + PaginationState, } from "@tanstack/react-table"; import { useOIDCContext } from "../../hooks/oidcConfiguration"; import { DataTable, MenuItem } from "../shared/DataTable"; import { Job, JobHistory, SearchBody } from "../../types"; import { useDiracxUrl } from "../../hooks/utils"; -import { useApplicationId } from "../../hooks/application"; import { JobHistoryDialog } from "./JobHistoryDialog"; import { @@ -54,17 +38,52 @@ import { } from "./JobDataService"; /** - * The data grid for the jobs + * Job Data Table props + * @property {number} searchBody - the search body to send along with the request + * @property {function} setSearchBody - the function to call when the search body changes */ -export function JobDataTable() { - const theme = useTheme(); - - // Id of the application - const appId = useApplicationId(); - - // Load the initial state from local storage - const initialState = sessionStorage.getItem(`${appId}_State`); +interface JobDataTableProps { + /** The search body to send along with the request */ + searchBody: SearchBody; + /** The function to call when the search body changes */ + setSearchBody: React.Dispatch>; + /** Columns */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columns: ColumnDef[]; + /** Pagination */ + pagination: PaginationState; + /** Set pagination */ + setPagination: React.Dispatch>; + /** Row selection */ + rowSelection: RowSelectionState; + /** Set row selection */ + setRowSelection: React.Dispatch>; + /** Column Visibility */ + columnVisibility: VisibilityState; + /** Set column visibility */ + setColumnVisibility: React.Dispatch>; + /** Column Pinning */ + columnPinning: ColumnPinningState; + /** Set column pinning */ + setColumnPinning: React.Dispatch>; +} +/** + * The data grid for the jobs + */ +export function JobDataTable({ + searchBody, + setSearchBody, + columns, + pagination, + setPagination, + rowSelection, + setRowSelection, + columnVisibility, + setColumnVisibility, + columnPinning, + setColumnPinning, +}: JobDataTableProps) { // Authentication const { configuration } = useOIDCContext(); const { accessToken } = useOidcAccessToken(configuration?.scope); @@ -79,47 +98,6 @@ export function JobDataTable() { severity: "success", }); - const parsedInitialState = - typeof initialState === "string" ? JSON.parse(initialState) : null; - - const [columnVisibility, setColumnVisibility] = useState( - parsedInitialState - ? parsedInitialState.columnVisibility - : { - JobGroup: false, - JobType: false, - Owner: false, - OwnerGroup: false, - VO: false, - StartExecTime: false, - EndExecTime: false, - UserPriority: false, - }, - ); - const [columnPinning, setColumnPinning] = useState( - parsedInitialState - ? parsedInitialState.columnPinning - : { - left: ["JobID"], // Pin JobID column by default - }, - ); - const [rowSelection, setRowSelection] = useState( - parsedInitialState ? parsedInitialState.rowSelection : {}, - ); - const [pagination, setPagination] = useState( - parsedInitialState - ? parsedInitialState.pagination - : { - pageIndex: 0, - pageSize: 25, - }, - ); - - // State for search body - const [searchBody, setSearchBody] = useState({ - sort: [{ parameter: "JobID", direction: "desc" }], - }); - // State for selected job const [selectedJobId, setSelectedJobId] = useState(null); @@ -127,143 +105,6 @@ export function JobDataTable() { const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false); const [jobHistoryData, setJobHistoryData] = useState([]); - // Save the state of the table in local storage - useEffect(() => { - const state = { - columnVisibility: { ...columnVisibility }, - columnPinning: { - left: [...(columnPinning.left || [])], - right: [...(columnPinning.right || [])], - }, - rowSelection: { ...rowSelection }, - pagination: { - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - }, - }; - - sessionStorage.setItem(`${appId}_State`, JSON.stringify(state)); - }, [columnVisibility, columnPinning, rowSelection, pagination]); - - // Status colors - const statusColors: Record = useMemo( - () => ({ - Submitting: purple[500], - Received: blueGrey[500], - Checking: teal[500], - Staging: lightBlue[500], - Waiting: amber[600], - Matched: blue[300], - Running: blue[900], - Rescheduled: lime[700], - Completing: orange[500], - Completed: green[300], - Done: green[500], - Failed: red[500], - Stalled: amber[900], - Killed: red[900], - Deleted: grey[500], - }), - [], - ); - - /** - * Renders the status cell with colors - */ - const renderStatusCell = useCallback( - (status: string) => { - return ( - - {status} - - ); - }, - [theme, statusColors], - ); - - const columnHelper = useMemo(() => createColumnHelper(), []); - - /** - * The head cells for the data grid (desktop version) - */ - const columns = useMemo( - () => [ - columnHelper.accessor("JobID", { - header: "ID", - meta: { type: "number" }, - }), - columnHelper.accessor("Status", { - header: "Status", - cell: (info) => renderStatusCell(info.getValue()), - meta: { type: "category", values: Object.keys(statusColors).sort() }, - }), - columnHelper.accessor("MinorStatus", { - header: "Minor Status", - }), - columnHelper.accessor("ApplicationStatus", { - header: "Application Status", - }), - columnHelper.accessor("Site", { - header: "Site", - }), - columnHelper.accessor("JobName", { - header: "Name", - }), - columnHelper.accessor("JobGroup", { - header: "Job Group", - }), - columnHelper.accessor("JobType", { - header: "Type", - }), - columnHelper.accessor("LastUpdateTime", { - header: "Last Update Time", - meta: { type: "date" }, - }), - columnHelper.accessor("HeartBeatTime", { - header: "Last Sign of Life", - meta: { type: "date" }, - }), - columnHelper.accessor("SubmissionTime", { - header: "Submission Time", - meta: { type: "date" }, - }), - columnHelper.accessor("Owner", { - header: "Owner", - }), - columnHelper.accessor("OwnerGroup", { - header: "Owner Group", - }), - columnHelper.accessor("VO", { - header: "VO", - }), - columnHelper.accessor("StartExecTime", { - header: "Start Execution Time", - meta: { type: "date" }, - }), - columnHelper.accessor("EndExecTime", { - header: "End Execution Time", - meta: { type: "date" }, - }), - columnHelper.accessor("UserPriority", { - header: "User Priority", - meta: { type: "number" }, - }), - ], - [columnHelper, renderStatusCell, statusColors], - ); - /** * Fetches the jobs from the /api/jobs/search endpoint */ diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx index 7b1819d2..dceaf94a 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobMonitor.tsx @@ -1,7 +1,31 @@ "use client"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + blue, + orange, + grey, + green, + red, + lightBlue, + purple, + teal, + blueGrey, + lime, + amber, +} from "@mui/material/colors"; +import { lighten, darken, useTheme, Box } from "@mui/material"; +import { + createColumnHelper, + ColumnPinningState, + RowSelectionState, + VisibilityState, + PaginationState, +} from "@tanstack/react-table"; -import { Box } from "@mui/material"; import { useApplicationId } from "../../hooks/application"; +import { FilterToolbar } from "../shared/FilterToolbar"; +import { InternalFilter } from "../../types/Filter"; +import { Job, SearchBody } from "../../types"; import { JobDataTable } from "./JobDataTable"; /** * Build the Job Monitor application @@ -10,6 +34,262 @@ import { JobDataTable } from "./JobDataTable"; */ export default function JobMonitor() { const appId = useApplicationId(); + const theme = useTheme(); + + // Load the initial state from local storage + const initialState = sessionStorage.getItem(`${appId}_State`); + + const parsedInitialState = + typeof initialState === "string" ? JSON.parse(initialState) : null; + + // State for filters + const [filters, setFilters] = useState( + parsedInitialState ? parsedInitialState.filters : [], + ); + + // State for search body + const [searchBody, setSearchBody] = useState({ + search: parsedInitialState + ? parsedInitialState.filters.map((filter: InternalFilter) => ({ + parameter: filter.parameter, + operator: filter.operator, + value: filter.value, + values: filter.values, + })) + : [], // Default to an empty array if no filters are present + sort: [{ parameter: "JobID", direction: "desc" }], + }); + + // States for table settings + const [columnVisibility, setColumnVisibility] = useState( + parsedInitialState + ? parsedInitialState.columnVisibility + : { + JobGroup: false, + JobType: false, + Owner: false, + OwnerGroup: false, + VO: false, + StartExecTime: false, + EndExecTime: false, + UserPriority: false, + }, + ); + const [columnPinning, setColumnPinning] = useState( + parsedInitialState + ? parsedInitialState.columnPinning + : { + left: ["JobID"], // Pin JobID column by default + }, + ); + const [rowSelection, setRowSelection] = useState( + parsedInitialState ? parsedInitialState.rowSelection : {}, + ); + const [pagination, setPagination] = useState( + parsedInitialState + ? parsedInitialState.pagination + : { + pageIndex: 0, + pageSize: 25, + }, + ); + + // Save the state of the app in local storage + useEffect(() => { + const state = { + filters: [...filters.filter((filter) => filter.isApplied)], + columnVisibility: { ...columnVisibility }, + columnPinning: { + left: [...(columnPinning.left || [])], + right: [...(columnPinning.right || [])], + }, + rowSelection: { ...rowSelection }, + pagination: { + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }, + }; + + sessionStorage.setItem(`${appId}_State`, JSON.stringify(state)); + }, [ + appId, + filters, + columnVisibility, + columnPinning, + rowSelection, + pagination, + ]); + + // Handle the application of filters + const handleApplyFilters = () => { + // Switch the applied state of the filters + setFilters((filters) => + filters.map((filter) => ({ ...filter, isApplied: true })), + ); + + setSearchBody((prev) => ({ + ...prev, + search: filters.map(({ parameter, operator, value, values }) => ({ + parameter, + operator, + value, + values, + })), + })); + setPagination((prevState) => ({ + ...prevState, + pageIndex: 0, + })); + }; + + const handleRemoveAllFilters = useCallback(() => { + setSearchBody((prevState) => ({ + ...prevState, + search: [], + })); + setPagination((prevState) => ({ + ...prevState, + pageIndex: 0, + })); + setFilters([]); + }, [setFilters]); + + // Status colors + const statusColors: Record = useMemo( + () => ({ + Submitting: purple[500], + Received: blueGrey[500], + Checking: teal[500], + Staging: lightBlue[500], + Waiting: amber[600], + Matched: blue[300], + Running: blue[900], + Rescheduled: lime[700], + Completing: orange[500], + Completed: green[300], + Done: green[500], + Failed: red[500], + Stalled: amber[900], + Killed: red[900], + Deleted: grey[500], + }), + [], + ); + + /** + * Renders the status cell with colors + */ + const renderStatusCell = useCallback( + (status: string) => { + return ( + + {status} + + ); + }, + [theme, statusColors], + ); + + const columnHelper = useMemo(() => createColumnHelper(), []); + + /** + * The head cells for the data grid (desktop version) + */ + const columns = useMemo( + () => [ + columnHelper.accessor("JobID", { + id: "JobID", + header: "ID", + meta: { type: "number" }, + }), + columnHelper.accessor("Status", { + id: "Status", + header: "Status", + cell: (info) => renderStatusCell(info.getValue()), + meta: { type: "category", values: Object.keys(statusColors).sort() }, + }), + columnHelper.accessor("MinorStatus", { + id: "MinorStatus", + header: "Minor Status", + }), + columnHelper.accessor("ApplicationStatus", { + id: "ApplicationStatus", + header: "Application Status", + }), + columnHelper.accessor("Site", { + id: "Site", + header: "Site", + }), + columnHelper.accessor("JobName", { + id: "JobName", + header: "Name", + }), + columnHelper.accessor("JobGroup", { + id: "JobGroup", + header: "Job Group", + }), + columnHelper.accessor("JobType", { + id: "JobType", + header: "Type", + }), + columnHelper.accessor("LastUpdateTime", { + id: "LastUpdateTime", + header: "Last Update Time", + meta: { type: "date" }, + }), + columnHelper.accessor("HeartBeatTime", { + id: "HeartBeatTime", + header: "Last Sign of Life", + meta: { type: "date" }, + }), + columnHelper.accessor("SubmissionTime", { + id: "SubmissionTime", + header: "Submission Time", + meta: { type: "date" }, + }), + columnHelper.accessor("Owner", { + id: "Owner", + header: "Owner", + }), + columnHelper.accessor("OwnerGroup", { + id: "OwnerGroup", + header: "Owner Group", + }), + columnHelper.accessor("VO", { + id: "VO", + header: "VO", + }), + columnHelper.accessor("StartExecTime", { + id: "StartExecTime", + header: "Start Execution Time", + meta: { type: "date" }, + }), + columnHelper.accessor("EndExecTime", { + id: "EndExecTime", + header: "End Execution Time", + meta: { type: "date" }, + }), + columnHelper.accessor("UserPriority", { + id: "UserPriority", + header: "User Priority", + meta: { type: "number" }, + }), + ], + [columnHelper, renderStatusCell, statusColors], + ); + return ( - {/* The key is used to force a re-render of the component when the appId changes */} - + + ); } diff --git a/packages/diracx-web-components/src/components/shared/DataTable.tsx b/packages/diracx-web-components/src/components/shared/DataTable.tsx index ea6a5a63..aae7e16f 100644 --- a/packages/diracx-web-components/src/components/shared/DataTable.tsx +++ b/packages/diracx-web-components/src/components/shared/DataTable.tsx @@ -1,11 +1,5 @@ "use client"; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { alpha } from "@mui/material/styles"; import Box from "@mui/material/Box"; import Table from "@mui/material/Table"; @@ -37,11 +31,7 @@ import { } from "@mui/material"; import { flexRender, Row, Table as TanstackTable } from "@tanstack/react-table"; import { TableComponents, TableVirtuoso } from "react-virtuoso"; -import { InternalFilter } from "../../types/Filter"; -import { useSearchParamsUtils } from "../../hooks/searchParamsUtils"; -import { ApplicationsContext } from "../../contexts/ApplicationsProvider"; -import { DashboardGroup, SearchBody } from "../../types"; -import { FilterToolbar } from "./FilterToolbar"; +import { SearchBody } from "../../types"; /** * Menu item @@ -263,119 +253,6 @@ export function DataTable>({ id: number | null; }>({ mouseX: null, mouseY: null, id: null }); - // State for the search parameters - const { getParam, setParam } = useSearchParamsUtils(); - const appId = getParam("appId"); - - // State for filters - const [filters, setFilters] = useState([]); - const [appliedFilters, setAppliedFilters] = - useState(filters); - - const updateFiltersAndUrl = useCallback( - (newFilters: InternalFilter[]) => { - // Update the filters in the URL using the setParam function - setParam( - "filter", - newFilters.map( - (filter) => - `${filter.id}_${filter.parameter}_${filter.operator}_${filter.value}`, - ), - ); - }, - [setParam], - ); - - // State for the user dashboard - const [userDashboard, setUserDashboard] = useContext(ApplicationsContext); - const updateGroupFilters = useCallback( - (newFilters: InternalFilter[]) => { - const appId = getParam("appId"); - - const group = userDashboard.find((group) => - group.items.some((item) => item.id === appId), - ); - if (group) { - const newGroup = { - ...group, - items: group.items.map((item) => { - if (item.id === appId) { - return { ...item, data: newFilters }; - } - return item; - }), - }; - setUserDashboard((groups: DashboardGroup[]) => - groups.map((s) => (s.title === group.title ? newGroup : s)), - ); - } - }, - [getParam, userDashboard, setUserDashboard], - ); - - // Handle the application of filters - const handleApplyFilters = () => { - // Transform list of internal filters into filters - const jsonFilters = filters.map((filter) => ({ - parameter: filter.parameter, - operator: filter.operator, - value: filter.value, - values: filter.values, - })); - setSearchBody((prevState) => ({ - ...prevState, - search: jsonFilters, - })); - table.setPageIndex(0); - setAppliedFilters(filters); - - updateFiltersAndUrl(filters); - updateGroupFilters(filters); - }; - - const handleRemoveAllFilters = useCallback(() => { - setSearchBody((prevState) => ({ - ...prevState, - search: [], - })); - table.setPageIndex(0); - setAppliedFilters([]); - - updateFiltersAndUrl([]); - updateGroupFilters([]); - }, [setFilters]); - - const DashboardItem = useMemo( - () => - userDashboard - .find((group) => group.items.some((item) => item.id === appId)) - ?.items.find((item) => item.id === appId), - [appId, userDashboard], - ); - - useEffect(() => { - if (DashboardItem?.data) { - setFilters(DashboardItem.data); - setAppliedFilters(DashboardItem.data); - const jsonFilters = DashboardItem.data.map((filter: InternalFilter) => ({ - parameter: filter.parameter, - operator: filter.operator, - value: filter.value, - values: filter.values, - })); - setSearchBody((prevState) => ({ - ...prevState, - search: jsonFilters, - })); - } else { - setFilters([]); - setSearchBody((prevState) => ({ - ...prevState, - search: [], - })); - } - }, [DashboardItem?.data, setFilters, setSearchBody]); - // Manage sorting const handleRequestSort = useCallback( (_event: React.MouseEvent, property: string) => { @@ -473,35 +350,25 @@ export function DataTable>({ if (isValidating || isLoading || error || noData) { return ( - <> - - - {isValidating || isLoading ? ( - - ) : error ? ( - - An error occurred while fetching data. Reload the page. - - ) : ( - - No data or no results match your filters. - - )} - - + + {isValidating || isLoading ? ( + + ) : error ? ( + + An error occurred while fetching data. Reload the page. + + ) : ( + + No data or no results match your filters. + + )} + ); } @@ -516,15 +383,6 @@ export function DataTable>({ overflow: "hidden", }} > - - > { /** The columns of the data table */ - columns: Column[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columns: ColumnDef[]; /** The function to call when a filter is changed */ handleFilterChange: (index: number, tempFilter: InternalFilter) => void; /** The function to call when the filter menu is closed */ @@ -68,6 +69,7 @@ export function FilterForm>({ parameter: "", operator: "eq", value: "", + isApplied: false, }); } }, [filters, filterIndex]); @@ -99,8 +101,8 @@ export function FilterForm>({ const selectedColumn = columns.find((c) => c.id == tempFilter.parameter); - const columnType = selectedColumn?.columnDef.meta?.type || "default"; - const isCategory = Array.isArray(selectedColumn?.columnDef.meta?.values); + const columnType = selectedColumn?.meta?.type || "default"; + const isCategory = Array.isArray(selectedColumn?.meta?.values); const isDateTime = columnType === "date"; const isNumber = columnType === "number"; @@ -237,7 +239,7 @@ export function FilterForm>({ multiple={isMultiple} sx={{ minWidth: 100 }} > - {selectedColumn?.columnDef.meta?.values?.map((val: string) => ( + {selectedColumn?.meta?.values?.map((val: string) => ( {val} @@ -300,7 +302,7 @@ export function FilterForm>({ onChange("parameter", parameter); const column = columns.find((v) => v.id === parameter); - const colType = column?.columnDef.meta?.type || "default"; + const colType = column?.meta?.type || "default"; const typeKey = colType === "date" ? "date" @@ -320,10 +322,10 @@ export function FilterForm>({ > {columns.map((column) => ( - {column.columnDef.header?.toString()} + {column.header?.toString()} ))} diff --git a/packages/diracx-web-components/src/components/shared/FilterToolbar.tsx b/packages/diracx-web-components/src/components/shared/FilterToolbar.tsx index 81efcea1..2e327dbe 100644 --- a/packages/diracx-web-components/src/components/shared/FilterToolbar.tsx +++ b/packages/diracx-web-components/src/components/shared/FilterToolbar.tsx @@ -6,7 +6,7 @@ import { FilterList, Delete, Send, Refresh } from "@mui/icons-material"; import Chip from "@mui/material/Chip"; import Button from "@mui/material/Button"; import { Alert, Box, Popover, Stack, Tooltip } from "@mui/material"; -import { Column } from "@tanstack/react-table"; +import { ColumnDef } from "@tanstack/react-table"; import { InternalFilter } from "../../types/Filter"; import { FilterForm } from "./FilterForm"; import "../../hooks/theme"; @@ -17,13 +17,12 @@ import "../../hooks/theme"; */ export interface FilterToolbarProps> { /** The columns of the data table */ - columns: Column[]; - /** The filters to apply */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columns: ColumnDef[]; + /** The filters */ filters: InternalFilter[]; /** The function to set the filters */ setFilters: React.Dispatch>; - /** The applied filters */ - appliedFilters: InternalFilter[]; /** The function to apply the filters */ handleApplyFilters: () => void; /** The function to remove all filters */ @@ -39,7 +38,6 @@ export function FilterToolbar>({ columns, filters, setFilters, - appliedFilters, handleApplyFilters, handleClearFilters, }: FilterToolbarProps) { @@ -58,6 +56,7 @@ export function FilterToolbar>({ parameter: "", operator: "eq", value: "", + isApplied: false, }; setSelectedFilter(newFilter); setAnchorEl(addFilterButtonRef.current); @@ -93,15 +92,8 @@ export function FilterToolbar>({ }; const changesUnapplied = useCallback(() => { - return JSON.stringify(filters) !== JSON.stringify(appliedFilters); - }, [filters, appliedFilters]); - - const isApplied = useCallback( - (filter: InternalFilter) => { - return appliedFilters.some((f) => f.id == filter.id); - }, - [appliedFilters], - ); + return filters.some((filter) => !filter.isApplied); + }, [filters]); function debounce void>( func: T, @@ -214,12 +206,10 @@ export function FilterToolbar>({ }} sx={{ m: 0.5, - backgroundColor: isApplied(filter) ? "primary.main" : grey[500], + backgroundColor: filter.isApplied ? "primary.main" : grey[500], }} className={ - isApplied(filter) - ? "chip-filter-applied" - : "chip-filter-unapplied" + filter.isApplied ? "chip-filter-applied" : "chip-filter-unapplied" } /> ))} diff --git a/packages/diracx-web-components/src/types/DashboardItem.ts b/packages/diracx-web-components/src/types/DashboardItem.ts index 3b69eeaf..5cab1b17 100644 --- a/packages/diracx-web-components/src/types/DashboardItem.ts +++ b/packages/diracx-web-components/src/types/DashboardItem.ts @@ -1,7 +1,6 @@ "use client"; import { SvgIconComponent } from "@mui/icons-material"; -import { InternalFilter } from "./Filter"; // Define the type for the Dashboard Item state export interface DashboardItem { @@ -9,5 +8,4 @@ export interface DashboardItem { type: string; id: string; icon: SvgIconComponent | null; - data?: InternalFilter[]; } diff --git a/packages/diracx-web-components/src/types/Filter.ts b/packages/diracx-web-components/src/types/Filter.ts index 3ac55876..1ae2e0b2 100644 --- a/packages/diracx-web-components/src/types/Filter.ts +++ b/packages/diracx-web-components/src/types/Filter.ts @@ -19,4 +19,5 @@ export interface Filter { */ export interface InternalFilter extends Filter { id: number; + isApplied: boolean; } diff --git a/packages/diracx-web-components/test/FilterForm.test.tsx b/packages/diracx-web-components/test/FilterForm.test.tsx index d91d9b42..9356581f 100644 --- a/packages/diracx-web-components/test/FilterForm.test.tsx +++ b/packages/diracx-web-components/test/FilterForm.test.tsx @@ -1,10 +1,6 @@ import React from "react"; import { render, screen, fireEvent, within } from "@testing-library/react"; -import { - createColumnHelper, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; +import { createColumnHelper } from "@tanstack/react-table"; import { FilterForm } from "../src/components/shared/FilterForm"; import { ThemeProvider } from "../src/contexts/ThemeProvider"; @@ -22,33 +18,37 @@ describe("FilterForm", () => { const columnDefs = [ columnHelper.accessor("id", { + id: "id", header: "ID", meta: { type: "number" }, }), columnHelper.accessor("name", { + id: "name", header: "Name", meta: { type: "string" }, }), columnHelper.accessor("category", { + id: "category", header: "Category", meta: { type: "category", values: ["A", "B", "C"] }, // Example of a category column }), columnHelper.accessor("date", { + id: "date", header: "Date", meta: { type: "date" }, // Example of a DateTime column }), ]; - // Create mock data for the table - const data: SimpleItem[] = [ - { id: 1, name: "Item 1", category: "A", date: new Date() }, - { id: 2, name: "Item 2", category: "B", date: new Date() }, - ]; - // Mock filters const filters = [ - { id: 1, parameter: "id", operator: "eq", value: "4" }, - { id: 2, parameter: "name", operator: "neq", value: "value2" }, + { id: 1, parameter: "id", operator: "eq", value: "4", isApplied: false }, + { + id: 2, + parameter: "name", + operator: "neq", + value: "value2", + isApplied: false, + }, ]; const setFilters = jest.fn(); const handleFilterChange = jest.fn(); @@ -62,16 +62,10 @@ describe("FilterForm", () => { const FilterFormWrapper: React.FC = ({ selectedFilterId, }) => { - const table = useReactTable({ - data, - columns: columnDefs, - getCoreRowModel: getCoreRowModel(), - }); - return ( - columns={table.getAllColumns()} + columns={columnDefs} filters={filters} setFilters={setFilters} handleFilterChange={handleFilterChange} @@ -150,7 +144,13 @@ describe("FilterForm", () => { expect(setFilters).toHaveBeenCalledWith([ ...filters, - { id: expect.any(Number), parameter: "", operator: "eq", value: "" }, + { + id: expect.any(Number), + parameter: "", + operator: "eq", + value: "", + isApplied: false, + }, ]); expect(handleFilterChange).not.toHaveBeenCalled(); expect(handleFilterMenuClose).toHaveBeenCalled(); @@ -178,6 +178,7 @@ describe("FilterForm", () => { parameter: "category", operator: "eq", value: "", + isApplied: false, }); expect(handleFilterMenuClose).toHaveBeenCalled(); }); diff --git a/packages/diracx-web-components/test/FilterToolbar.test.tsx b/packages/diracx-web-components/test/FilterToolbar.test.tsx index 527aa9b6..8097859a 100644 --- a/packages/diracx-web-components/test/FilterToolbar.test.tsx +++ b/packages/diracx-web-components/test/FilterToolbar.test.tsx @@ -42,12 +42,22 @@ describe("FilterToolbar", () => { // Create mock filters const filters = [ - { id: 1, parameter: "id", operator: "eq", value: "value1" }, - { id: 2, parameter: "name", operator: "neq", value: "value2" }, - ]; - const appliedFilters = [ - { id: 1, parameter: "id", operator: "eq", value: "value1" }, + { + id: 1, + parameter: "id", + operator: "eq", + value: "value1", + isApplied: true, + }, + { + id: 2, + parameter: "name", + operator: "neq", + value: "value2", + isApplied: false, + }, ]; + const setFilters = jest.fn(); const handleApplyFilters = jest.fn(); const handleClearFilters = jest.fn(); @@ -68,7 +78,6 @@ describe("FilterToolbar", () => { setFilters={setFilters} handleApplyFilters={handleApplyFilters} handleClearFilters={handleClearFilters} - appliedFilters={appliedFilters} /> ); @@ -104,19 +113,14 @@ describe("FilterToolbar", () => { expect(warningMessage).toBeInTheDocument(); - appliedFilters.push({ - id: 2, - parameter: "name", - operator: "neq", - value: "value2", - }); + filters[1].isApplied = true; cleanup(); render(); expect(warningMessage).not.toBeInTheDocument(); - appliedFilters.pop(); + filters[1].isApplied = false; }); it("opens the filter form when 'Add filter' button is clicked", () => { diff --git a/packages/diracx-web-components/test/JobDataTable.test.tsx b/packages/diracx-web-components/test/JobDataTable.test.tsx index ba3a302f..4b8f1e18 100644 --- a/packages/diracx-web-components/test/JobDataTable.test.tsx +++ b/packages/diracx-web-components/test/JobDataTable.test.tsx @@ -1,80 +1,146 @@ import React from "react"; import { render } from "@testing-library/react"; -import useSWR from "swr"; -import { useOidcAccessToken } from "@axa-fr/react-oidc"; import { VirtuosoMockContext } from "react-virtuoso"; +import { useOidcAccessToken } from "@axa-fr/react-oidc"; +import { createColumnHelper } from "@tanstack/react-table"; import { JobDataTable } from "../src/components/JobMonitor/JobDataTable"; +import { useJobs } from "../src/components/JobMonitor/JobDataService"; +import { Job } from "../src/types"; -// Mock modules +// ——— Mock out OIDC + DataService hooks/funcs ——— jest.mock("@axa-fr/react-oidc", () => ({ useOidcAccessToken: jest.fn(), })); -jest.mock("swr", () => jest.fn()); - -// In your test file or a Jest setup file -jest.mock("jsoncrush", () => ({ - crush: jest.fn().mockImplementation((data) => `crushed-${data}`), - uncrush: jest.fn().mockImplementation((data) => data.replace("crushed-", "")), +jest.mock("../src/components/JobMonitor/JobDataService", () => ({ + useJobs: jest.fn(), + deleteJobs: jest.fn(), + killJobs: jest.fn(), + rescheduleJobs: jest.fn(), + refreshJobs: jest.fn(), + getJobHistory: jest.fn(), })); +const columnHelper = createColumnHelper(); + +const columnDefs = [ + columnHelper.accessor("JobID", { + id: "JobID", + header: "Job ID", + meta: { type: "string" }, + }), + columnHelper.accessor("JobName", { + id: "JobName", + header: "Job Name", + meta: { type: "string" }, + }), + columnHelper.accessor("Status", { + id: "Status", + header: "Status", + meta: { type: "string" }, + }), + columnHelper.accessor("MinorStatus", { + id: "MinorStatus", + header: "Minor Status", + meta: { type: "string" }, + }), + columnHelper.accessor("SubmissionTime", { + id: "SubmissionTime", + header: "Submission Time", + meta: { type: "date" }, + }), +]; + +const defaultProps = { + searchBody: { + search: [], + sort: [{ parameter: "JobID", direction: "desc" as const }], + }, + setSearchBody: jest.fn(), + columns: columnDefs, + pagination: { pageIndex: 0, pageSize: 25 }, + setPagination: jest.fn(), + rowSelection: {}, + setRowSelection: jest.fn(), + columnVisibility: {}, + setColumnVisibility: jest.fn(), + columnPinning: { left: [] }, + setColumnPinning: jest.fn(), +}; + describe("", () => { - it("displays loading state when data is being validated", () => { - (useSWR as jest.Mock).mockReturnValue({ - data: null, + beforeEach(() => { + jest.resetAllMocks(); + // return shape matching: const { accessToken } = useOidcAccessToken(...) + (useOidcAccessToken as jest.Mock).mockReturnValue({ accessToken: "1234" }); + }); + + function renderWithProps(overrides = {}) { + return render( + + + , + ); + } + + it("displays the skeleton when `isValidating` is true", () => { + (useJobs as jest.Mock).mockReturnValue({ + data: undefined, error: null, isValidating: true, isLoading: false, }); - (useOidcAccessToken as jest.Mock).mockReturnValue("1234"); - const { getByTestId } = render(); + const { getByTestId } = renderWithProps(); expect(getByTestId("loading-skeleton")).toBeVisible(); }); - it("displays loading state when data is being loaded", () => { - (useSWR as jest.Mock).mockReturnValue({ - data: null, + it("displays the skeleton when `isLoading` is true", () => { + (useJobs as jest.Mock).mockReturnValue({ + data: undefined, error: null, isValidating: false, isLoading: true, }); - (useOidcAccessToken as jest.Mock).mockReturnValue("1234"); - const { getByTestId } = render(); + const { getByTestId } = renderWithProps(); expect(getByTestId("loading-skeleton")).toBeVisible(); }); - it("displays error state", () => { - (useSWR as jest.Mock).mockReturnValue({ - data: null, - error: true, + it("displays the error message", () => { + (useJobs as jest.Mock).mockReturnValue({ + data: undefined, + error: new Error("fetch-fail"), isValidating: false, isLoading: false, }); - const { getByText } = render(); + const { getByText } = renderWithProps(); expect( getByText("An error occurred while fetching data. Reload the page."), ).toBeInTheDocument(); }); - it("displays no jobs data state", () => { - (useSWR as jest.Mock).mockReturnValue({ - data: [], - error: false, + it("displays the no-data message when `data.data` is empty", () => { + (useJobs as jest.Mock).mockReturnValue({ + data: { headers: new Headers(), data: [] }, + error: null, isValidating: false, isLoading: false, }); - const { getByText } = render(); + const { getByText } = renderWithProps(); expect( getByText("No data or no results match your filters."), ).toBeInTheDocument(); }); - it("displays jobs data in the grid", () => { - const mockData = { + it("renders rows when there is job data", () => { + const headers = new Headers({ "content-range": "jobs 0-0/1" }); + const fakeData = { + headers, data: [ { JobID: "1", @@ -85,22 +151,15 @@ describe("", () => { }, ], }; - (useSWR as jest.Mock).mockReturnValue({ - data: mockData, - error: false, + + (useJobs as jest.Mock).mockReturnValue({ + data: fakeData, + error: null, isValidating: false, isLoading: false, }); - const { getByText } = render(, { - wrapper: ({ children }) => ( - - {children} - - ), - }); + const { getByText } = renderWithProps(); expect(getByText("TestJob1")).toBeInTheDocument(); }); }); diff --git a/packages/diracx-web/src/app/(dashboard)/page.tsx b/packages/diracx-web/src/app/(dashboard)/page.tsx index 055bc49d..0a0f2cfd 100644 --- a/packages/diracx-web/src/app/(dashboard)/page.tsx +++ b/packages/diracx-web/src/app/(dashboard)/page.tsx @@ -23,5 +23,5 @@ export default function Page() { return applicationList.find((app) => app.name === appType)?.component; }, [appType]); - return Component ? : ; + return Component ? : ; } diff --git a/packages/extensions/src/app/(dashboard)/page.tsx b/packages/extensions/src/app/(dashboard)/page.tsx index bafe7433..22362513 100644 --- a/packages/extensions/src/app/(dashboard)/page.tsx +++ b/packages/extensions/src/app/(dashboard)/page.tsx @@ -26,5 +26,5 @@ export default function Page() { }, [appType]); // Render the component if it exists, otherwise render the UserDashboard - return Component ? : ; + return Component ? : ; }