@maap-jupyterlab/dps-jupyter-extension
Version:
A JupyterLab extension for submitting and viewing jobs.
347 lines (346 loc) • 17.2 kB
JavaScript
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))));
};