UNPKG

@maap-jupyterlab/dps-jupyter-extension

Version:

A JupyterLab extension for submitting and viewing jobs.

347 lines (346 loc) 17.2 kB
import React, { useMemo, useEffect, useState, Fragment } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useTable, useGlobalFilter, useSortBy, usePagination, useFilters, useAsyncDebounce } from 'react-table'; import { Pagination, Spinner, Table, Button, InputGroup, FormControl, ButtonGroup } from 'react-bootstrap'; import { JobStatusBadge } from './JobStatusBadge'; import { FaSort, FaSortDown, FaSortUp } from 'react-icons/fa'; import { Search } from 'react-bootstrap-icons'; import { jobsActions, selectJobs } from '../redux/slices/jobsSlice'; import { selectJobsContainer } from '../redux/slices/JobsContainerSlice'; import { selectUserInfo } from '../redux/slices/userInfoSlice'; import { parseJobData } from '../utils/mapping'; import { getUserJobs } from '../api/maap_py'; import { openSubmitJobs } from '../utils/utils'; import "../../style/JobsOverview.css"; import { MdClear, MdFilterAlt, MdFilterAltOff, MdRefresh } from 'react-icons/md'; export const JobsOverviewContainer = ({ jupyterApp }) => { // Redux const dispatch = useDispatch(); const { itemSize } = useSelector(selectJobsContainer); const { selectedJob, userJobInfo, jobRefreshTimestamp } = useSelector(selectJobs); const { username } = useSelector(selectUserInfo); const { setSelectedJob, setUserJobInfo, setJobRefreshTimestamp } = jobsActions; // Local component variables const [showSpinner, setShowSpinner] = useState(false); const [showFilters, setShowFilters] = useState(true); const [statusFilterOptions, setStatusFilterOptions] = useState([]); const data = useMemo(() => userJobInfo, [userJobInfo]); useEffect(() => { getJobInfo(); }, []); useEffect(() => { setPageSize(itemSize); }, [itemSize]); const getJobInfo = () => { setShowSpinner(true); // List all jobs for a given user let response = getUserJobs(username); response.then((data) => { dispatch(setUserJobInfo(parseJobData(data["response"]["jobs"]))); }).finally(() => { setShowSpinner(false); // Update refresh timestamp dispatch(setJobRefreshTimestamp(new Date().toUTCString())); // setGlobalFilter(state.globalFilter) }); }; // Set selected row const handleRowClick = (row) => { userJobInfo.map(job => { if (job['payload_id'] === row.values.payload_id) { dispatch(setSelectedJob({ rowIndex: row.index, jobID: row.values.payload_id, jobInfo: job })); return; } }); }; const MultipleFilter = (rows, filler, filterValue) => { const arr = []; rows.forEach((val) => { if (filterValue.includes(val.original.status)) arr.push(val); }); return arr; }; function setFilteredParams(filterArr, val) { console.log("Check filter"); console.log(filterArr); console.log(val); if (filterArr.includes(val)) { console.log("test"); filterArr = filterArr.filter((n) => { return n !== val; }); } else { console.log("test2"); filterArr.push(val); } ; //if (filterArr.length === 0) filterArr = undefined; return filterArr; } const SelectColumnFilter = ({ column: { filterValue = [], setFilter, preFilteredRows, id } }) => { const options = useMemo(() => { const options = new Set(); preFilteredRows.forEach((row) => { options.add(row.values[id]); }); // this line is causing a console error but dont see a better way around it setStatusFilterOptions(Array.from(options)); return [...options.values()]; }, [id, preFilteredRows]); return (React.createElement(Fragment, null, React.createElement("div", { className: "block" }, options.map((option, i) => { return (React.createElement(Fragment, { key: i }, React.createElement("div", { className: "flex items-center" }, React.createElement("input", { type: "checkbox", defaultChecked: true, className: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded", id: option, name: option, value: option, onChange: (e) => { setFilter(setFilteredParams(filterValue, e.target.value)); } }), React.createElement("label", { htmlFor: option, className: "ml-1.5 font-medium text-gray-700" }, option)))); })))); }; // Text filter for a single column const TextColumnFilter = ({ column: { filterValue, preFilteredRows, setFilter } }) => { const count = preFilteredRows.length; return (React.createElement("div", { className: 'text-filter' }, React.createElement("input", { value: filterValue || '', onChange: e => { setFilter(e.target.value || undefined); }, placeholder: `Search ${count} record(s)...` }))); }; // Global text filter const GlobalFilter = ({ preGlobalFilteredRows, globalFilter, setGlobalFilter, }) => { const [value, setValue] = React.useState(globalFilter); const onChange = useAsyncDebounce(value => { setGlobalFilter(value || undefined); }, 200); return (React.createElement(InputGroup, { className: "mb-3" }, React.createElement(InputGroup.Text, { id: "basic-addon1" }, React.createElement(Search, null)), React.createElement(FormControl, { type: "search", key: "globalSearch", placeholder: `Search records...`, "aria-label": "Search", "aria-describedby": "basic-addon1", value: value || "", onChange: e => { setValue(e.target.value); onChange(e.target.value); } }))); }; // Date range filter component for a single column const DateRangeColumnFilter = ({ column: { filterValue = [], preFilteredRows, setFilter, id } }) => { const [min, max] = React.useMemo(() => { let min = preFilteredRows.length ? new Date(preFilteredRows[0].values[id]) : new Date(0); let max = preFilteredRows.length ? new Date(preFilteredRows[0].values[id]) : new Date(0); preFilteredRows.forEach((row) => { const rowDate = new Date(row.values[id]); min = rowDate <= min ? rowDate : min; max = rowDate >= max ? rowDate : max; }); return [min, max]; }, [id, preFilteredRows]); return (React.createElement("div", { className: 'date-filter' }, React.createElement("input", { onChange: (e) => { const val = e.target.value; setFilter((old = []) => [val ? val : undefined, old[1]]); }, type: "date", value: filterValue[0] || "" }), " to ", React.createElement("input", { onChange: (e) => { const val = e.target.value; setFilter((old = []) => [ old[0], val ? val.concat("T23:59:59.999Z") : undefined ]); }, type: "date", value: filterValue[1]?.slice(0, 10) || "" }))); }; const testSort = (canSort, isSortedDesc) => { if (canSort) { if (isSortedDesc === undefined) { return React.createElement(FaSort, { size: 24 }); } else if (isSortedDesc) { return React.createElement(FaSortDown, { size: 24 }); } else { return React.createElement(FaSortUp, { size: 24 }); } } }; const dateSort = useMemo(() => (rowA, rowB, columnId) => { // TODO: case where endtime is null but start time is not. if (rowA.values[columnId] === "") { const a = new Date(rowA.values["time_queued"]); const b = new Date(rowB.values[columnId]); return a > b ? 1 : -1; //return -1 } if (rowB.values[columnId] === "") { const a = new Date(rowA.values[columnId]); const b = new Date(rowB.values["time_queued"]); return a > b ? 1 : -1; //return 1 } const a = new Date(rowA.values[columnId]); const b = new Date(rowB.values[columnId]); return a > b ? 1 : -1; }, []); // Date range filter function for a single column const dateBetweenFilterFn = (rows, id, filterValues) => { const sd = filterValues[0] ? new Date(filterValues[0]) : undefined; const ed = filterValues[1] ? new Date(filterValues[1]) : undefined; if (ed || sd) { return rows.filter((r) => { var dateAndHour = r.original[id[0]].split("T"); var [year, month, day] = dateAndHour[0].split("-"); var date = [month, day, year].join("/"); var hour = dateAndHour[1]; var formattedData = date + " " + hour; const cellDate = new Date(formattedData); if (ed && sd) { return cellDate >= sd && cellDate <= ed; } else if (sd) { return cellDate >= sd; } else { return cellDate <= ed; } }); } else { return rows; } }; const columns = useMemo(() => [ { Header: 'Tag', accessor: 'tags', disableSortBy: true, maxWidth: 350, Filter: TextColumnFilter }, { Header: () => (React.createElement("div", { style: { textAlign: "center" } }, "Job Type")), accessor: 'job_type', disableSortBy: true, maxWidth: 300, Filter: TextColumnFilter }, { Header: () => (React.createElement("div", { style: { textAlign: "center" } }, "Status")), accessor: 'status', disableSortBy: true, maxWidth: 300, Cell: ({ cell: { row: { values: { status } } } }) => React.createElement("div", { style: { textAlign: "center" } }, React.createElement(JobStatusBadge, { status: status })), Filter: SelectColumnFilter, filter: MultipleFilter }, { Header: () => (React.createElement("div", { style: { textAlign: "center" } }, "Queued Time")), accessor: 'time_queued', Cell: row => (React.createElement("div", { style: { textAlign: "center" } }, row.value)), sortType: dateSort, sortDescFirst: true, Filter: DateRangeColumnFilter, filter: dateBetweenFilterFn, maxWidth: 300, }, { Header: () => (React.createElement("div", { style: { textAlign: "center" } }, "Start Time")), accessor: 'time_start', Cell: row => (React.createElement("div", { style: { textAlign: "center" } }, row.value)), sortType: dateSort, Filter: DateRangeColumnFilter, filter: dateBetweenFilterFn, maxWidth: 300, }, { Header: () => (React.createElement("div", { style: { textAlign: "center" } }, "End Time")), accessor: 'time_end', Cell: row => (React.createElement("div", { style: { textAlign: "center" } }, row.value)), Filter: DateRangeColumnFilter, filter: dateBetweenFilterFn, sortType: dateSort, maxWidth: 300, }, { Header: 'Payload ID', accessor: 'payload_id', disableSortBy: true, maxWidth: 300, Filter: TextColumnFilter } ], []); const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow, state, preGlobalFilteredRows, setGlobalFilter, canPreviousPage, canNextPage, pageOptions, pageCount, gotoPage, nextPage, previousPage, setPageSize, visibleColumns, setAllFilters, state: { pageIndex, pageSize, sortBy } } = useTable({ columns, data: data, disableSortRemove: true, initialState: { pageIndex: 0, pageSize: itemSize, sortBy: [ { id: "time_queued", desc: true }, ], filters: [ { id: 'status', value: statusFilterOptions, } ], } }, useFilters, useGlobalFilter, useSortBy, usePagination); return (React.createElement("div", null, React.createElement("div", { className: "overview-header" }, React.createElement("div", null, React.createElement("h1", null, "My Jobs")), React.createElement("div", null, React.createElement(Button, { variant: "primary", onClick: () => openSubmitJobs(jupyterApp, null) }, "+ Submit New Job"))), React.createElement("div", { className: 'table-toolbar' }, React.createElement(ButtonGroup, { className: 'toolbar-btn' }, showFilters ? React.createElement("button", { title: "Hide filters", onClick: () => setShowFilters(!showFilters) }, React.createElement(MdFilterAltOff, null)) : React.createElement("button", { title: "Show filters", onClick: () => setShowFilters(!showFilters) }, React.createElement(MdFilterAlt, null)), React.createElement("button", { title: "Clear filters", onClick: () => setAllFilters([{ id: 'status', value: statusFilterOptions }]) }, React.createElement(MdClear, null)), React.createElement("button", { title: "Refresh job list", onClick: getJobInfo }, React.createElement(MdRefresh, null))), jobRefreshTimestamp ? React.createElement("div", { className: "refresh-timestamp" }, "Last updated:", React.createElement("br", null), " ", jobRefreshTimestamp) : ""), React.createElement("div", { className: "table-container" }, React.createElement(Table, Object.assign({}, getTableProps()), React.createElement("thead", null, headerGroups.map(headerGroup => (React.createElement("tr", Object.assign({}, headerGroup.getHeaderGroupProps(), { key: "Header Group" }), headerGroup.headers.map(column => (React.createElement("th", Object.assign({}, column.getHeaderProps()), React.createElement("span", Object.assign({ className: 'header-sort' }, column.getSortByToggleProps()), column.render('Header'), testSort(column.canSort, column.isSortedDesc)), React.createElement("div", null, showFilters ? column.canFilter ? column.render('Filter') : null : null)))))))), showSpinner ? React.createElement("tbody", null, React.createElement("tr", { key: "loading-jobs" }, React.createElement("td", { colSpan: columns.length, style: { textAlign: "center" } }, React.createElement(Spinner, { animation: "border", variant: "primary" })))) : React.createElement("tbody", Object.assign({}, getTableBodyProps()), page.map(row => { prepareRow(row); return (React.createElement("tr", { key: row.index, className: selectedJob && (selectedJob.rowIndex === row.index) ? "selected-row" : "", onClick: () => handleRowClick(row) }, row.cells.map(cell => { return (React.createElement("td", Object.assign({ className: "cell-overflow" }, cell.getCellProps({ style: { maxWidth: cell.column.maxWidth } })), cell.render('Cell'))); }))); })))), React.createElement("div", { className: 'pagination' }, React.createElement(Pagination, null, React.createElement(Pagination.First, { onClick: () => gotoPage(0), disabled: !canPreviousPage }), React.createElement(Pagination.Prev, { onClick: () => previousPage(), disabled: !canPreviousPage }), React.createElement(Pagination.Next, { onClick: () => nextPage(), disabled: !canNextPage }), React.createElement(Pagination.Last, { onClick: () => gotoPage(pageCount - 1), disabled: !canNextPage })), React.createElement("span", null, "Page ", pageOptions.length === 0 ? 0 : pageIndex + 1, " of ", pageOptions.length)))); };