@maap-jupyterlab/dps-jupyter-extension
Version:
A JupyterLab extension for submitting and viewing jobs.
297 lines (296 loc) • 15.6 kB
JavaScript
import React from 'react';
import { Button, ButtonToolbar, Form } from 'react-bootstrap';
import { AlertBox } from './Alerts';
import { useSelector, useDispatch } from 'react-redux';
import AsyncSelect from 'react-select/async';
import { useEffect, useState, useRef } from 'react';
import { selectCMRSwitch, CMRSwitchActions } from '../redux/slices/CMRSwitchSlice';
import { getAlgorithms, describeAlgorithms, getResources, getCMRCollections, submitJob, getUserJobs } from '../api/maap_py';
import { algorithmsActions, selectAlgorithms } from '../redux/slices/algorithmsSlice';
import { parseScienceKeywords } from '../utils/ogc_parsers';
import '../../style/JobSubmission.css';
import { Notification } from "@jupyterlab/apputils";
import { selectUserInfo, userInfoActions } from '../redux/slices/userInfoSlice';
import { jobsActions } from '../redux/slices/jobsSlice';
import { parseJobData } from '../utils/mapping';
import { copyNotebookCommand } from '../utils/utils';
import { SUBMITTING_JOB_TEXT, SUBMITTED_JOB_SUCCESS, SUBMITTED_JOB_FAIL, SUBMITTED_JOB_ELEMENT_ID } from '../constants';
export const JobSubmissionForm = ({ uname }) => {
// Redux
const dispatch = useDispatch();
const { setAlgorithm, setResource, setAlgorithmMetadata, setCMRCollection } = algorithmsActions;
const { setUserJobInfo, setJobRefreshTimestamp } = jobsActions;
const { selectedAlgorithm, selectedResource, selectedAlgorithmMetadata, selectedCMRCollection } = useSelector(selectAlgorithms);
const { username } = useSelector(selectUserInfo);
const { setUsername } = userInfoActions;
const { toggleValue, toggleDisabled } = CMRSwitchActions;
const { switchIsChecked, switchIsDisabled } = useSelector(selectCMRSwitch);
// Local state variables
const [jobTag, setJobTag] = useState('');
const [command, setCommand] = useState('');
const [showWaitCursor, setShowWaitCursor] = useState(false);
const jobSubmitForm = useRef(null);
useEffect(() => {
dispatch(setUsername(uname));
}, []);
useEffect(() => {
if (selectedAlgorithm != null) {
let res = describeAlgorithms(selectedAlgorithm["value"]);
res.then((data) => {
dispatch(setAlgorithmMetadata(data));
});
}
}, [selectedAlgorithm]);
useEffect(() => {
if (command != '') {
copyNotebookCommand(command);
}
}, [command]);
useEffect(() => {
let elems = document.getElementsByClassName("jl-ReactAppWidget");
let pElement = document.getElementById(SUBMITTED_JOB_ELEMENT_ID);
// Apply the css to the parent div
if (showWaitCursor) {
elems[0].classList.add('wait-cursor');
pElement.textContent = SUBMITTING_JOB_TEXT;
}
else {
elems[0].classList.remove('wait-cursor');
}
}, [showWaitCursor]);
const handleAlgorithmChange = value => {
dispatch(setAlgorithm(value));
};
const handleResourceChange = value => {
dispatch(setResource(value));
};
const handleCMRCollectionChange = value => {
dispatch(setCMRCollection(value));
};
const disableSubmitButton = () => {
let elemsButtons = document.getElementsByClassName("btn btn-primary");
for (let i = 0; i < elemsButtons.length; i++) {
if (elemsButtons[i].getAttribute("type") === "submit") {
elemsButtons[i].setAttribute("disabled", "true");
}
}
};
const enableSubmitButton = () => {
let elemsButtons = document.getElementsByClassName("btn btn-primary");
for (let i = 0; i < elemsButtons.length; i++) {
if (elemsButtons[i].getAttribute("type") === "submit" && elemsButtons[i].hasAttribute("disabled")) {
elemsButtons[i].removeAttribute("disabled");
}
}
};
const setSubmittedJobText = (success, id, errorMessage) => {
let pElement = document.getElementById(SUBMITTED_JOB_ELEMENT_ID);
if (success) {
pElement.textContent = SUBMITTED_JOB_SUCCESS.replace("{TIME}", new Date().toUTCString()).replace("{ID}", id);
}
else {
pElement.textContent = SUBMITTED_JOB_FAIL.replace("{TIME}", new Date().toUTCString()).replace("{ERROR}", errorMessage);
}
};
const onSubmit = (event) => {
disableSubmitButton();
event.preventDefault();
var jobParams = {
algo_id: null,
version: null,
queue: null,
username: null,
identifier: null
};
if (selectedAlgorithm) {
let algorithm = selectedAlgorithm.value.split(':');
jobParams.algo_id = algorithm[0];
jobParams.version = algorithm[1];
}
if (selectedResource) {
jobParams.queue = selectedResource.value;
}
jobParams.username = username;
jobParams.identifier = jobTag;
let data = new FormData(event.target);
for (const input of data.entries()) {
jobParams[input[0]] = input[1];
}
let formValidation = validateForm(jobParams);
if (!formValidation) {
// Submit job
submitJob(jobParams).then((data) => {
let msg = " Job submitted successfully. " + data['response'];
Notification.success(msg, { autoClose: false });
setSubmittedJobText(true, data['response'], null);
setTimeout(() => {
enableSubmitButton();
setShowWaitCursor(false);
}, 2000);
}).catch(error => {
Notification.error(error.message, { autoClose: false });
setSubmittedJobText(false, null, error.message);
setTimeout(() => {
enableSubmitButton();
setShowWaitCursor(false);
}, 2000);
});
// Refresh job list once job has been submitted
let response = getUserJobs(username);
response.then((data) => {
dispatch(setUserJobInfo(parseJobData(data["response"]["jobs"])));
}).finally(() => {
dispatch(setJobRefreshTimestamp(new Date().toUTCString()));
});
}
else {
Notification.error("Job failed to submit because " + formValidation, { autoClose: false });
setShowWaitCursor(false);
enableSubmitButton();
setSubmittedJobText(false, null, formValidation);
}
};
const validateForm = (params) => {
let errorMsg = "";
if (!params.algo_id) {
errorMsg = "missing algorithm selection.";
}
else if (!params.identifier) {
errorMsg = "missing job tag.";
}
else if (!params.queue) {
errorMsg = "missing resource selection.";
}
return errorMsg;
};
// Reset job form
const clearForm = () => {
dispatch(setAlgorithm(null));
dispatch(setResource(null));
dispatch(setAlgorithmMetadata(null));
setJobTag('');
if (!switchIsDisabled) {
dispatch(toggleDisabled());
}
if (switchIsChecked) {
dispatch(toggleValue());
}
const activeElement = document.activeElement;
if (activeElement)
activeElement.blur();
};
// Build job submission jupyter notebook command from user-provided selections
const buildNotebookCommand = () => {
// maap.submitJob({"algo_id": "test_algo", "username": "anonymous", "queue": "geospec-job_worker-32gb"})
let jobParams = {
algo_id: null,
version: null,
queue: null,
username: null,
identifier: null
};
if (selectedAlgorithm) {
let algorithm = selectedAlgorithm.value.split(':');
jobParams.algo_id = algorithm[0];
jobParams.version = algorithm[1];
}
if (selectedResource) {
jobParams.queue = selectedResource.value;
}
jobParams.username = username;
jobParams.identifier = jobTag;
let data = new FormData(jobSubmitForm.current);
let inputStr = "";
for (const input of data.entries()) {
jobParams[input[0]] = input[1];
if (inputStr == "") {
inputStr = input[0] + "=" + "\"" + input[1] + "\"";
}
else {
inputStr = inputStr + ",\n " + input[0] + "=" + "\"" + input[1] + "\"";
}
}
let tmp = "maap.submitJob(identifier=\"" + jobParams.identifier + "\",\n " +
"algo_id=\"" + jobParams.algo_id + "\",\n " +
"version=\"" + jobParams.version + "\",\n " +
"username=\"" + jobParams.username + "\",\n " +
"queue=\"" + jobParams.queue + "\",\n " + inputStr + ")";
setCommand(tmp);
const activeElement = document.activeElement;
if (activeElement)
activeElement.blur();
};
return (React.createElement("div", { className: "submit-wrapper" },
React.createElement(Form, { onSubmit: onSubmit, ref: jobSubmitForm },
React.createElement("h5", null, "1. General Information"),
React.createElement(Form.Group, { className: "mb-3 algorithm-input" },
React.createElement(Form.Label, null, "Algorithm"),
React.createElement(AsyncSelect, { menuPortalTarget: document.querySelector('body'), cacheOptions: true, defaultOptions: true, value: selectedAlgorithm, loadOptions: getAlgorithms, onChange: handleAlgorithmChange, placeholder: "Select algorithm..." })),
React.createElement(Form.Group, { className: "mb-3 algorithm-input" },
React.createElement(Form.Label, null, "Job Tag"),
React.createElement(Form.Control, { type: "text", value: jobTag, placeholder: "Enter job tag...", onChange: event => setJobTag(event.target.value) })),
React.createElement(Form.Group, { className: "mb-3 algorithm-input" },
React.createElement(Form.Label, null, "Resource"),
React.createElement(AsyncSelect, { cacheOptions: true, defaultOptions: true, value: selectedResource, loadOptions: getResources, onChange: handleResourceChange, placeholder: "Select resource..." })),
selectedAlgorithmMetadata ?
// If user has selected an algorithm, render inputs and CMR info
React.createElement(React.Fragment, null,
React.createElement("h5", null, "2. Algorithm Inputs"),
Array.isArray(selectedAlgorithmMetadata.inputs) ?
selectedAlgorithmMetadata.inputs.map((item, index) => {
switch (item["ows:Title"]) {
case "publish_to_cmr":
if (switchIsDisabled) {
dispatch(toggleDisabled());
}
return "publish to cmr";
case "queue_name":
return ""; // already integrated above using the resource async select
default:
return React.createElement("div", { key: `${item["ows:Title"]}_${index}` },
React.createElement(Form.Group, { className: "algorithm-input" },
React.createElement(Form.Label, null, `${item["ows:Title"]}`),
React.createElement(Form.Control
// value={item["ows:Title"]}
, {
// value={item["ows:Title"]}
name: item["ows:Title"], placeholder: `Enter ${item["ows:Title"]}...`, required: false })));
}
}) : React.createElement(AlertBox, { text: "No inputs defined for selected algorithm.", variant: "primary" }),
React.createElement("div", { className: "cmr" },
React.createElement("div", { style: { display: 'flex' } },
React.createElement("h5", null, "3. Publish to Content Metadata Repository (CMR)?"),
React.createElement(Form.Group, null,
React.createElement(Form.Switch
//custom
, {
//custom
disabled: switchIsDisabled, type: "switch", checked: switchIsChecked, onChange: () => dispatch(toggleValue()) }))),
switchIsChecked ?
React.createElement(Form.Group, { className: "mb-3 algorithm-input" },
React.createElement(Form.Label, null, "CMR Collections"),
React.createElement(AsyncSelect, { cacheOptions: true, defaultOptions: true, value: selectedCMRCollection, loadOptions: getCMRCollections, onChange: handleCMRCollectionChange, placeholder: "Select CMR collection..." }))
: null,
selectedCMRCollection && switchIsChecked ?
React.createElement("table", { className: "cmr-table" },
React.createElement("tr", { key: "concept-id" },
React.createElement("td", null, "Concept ID"),
React.createElement("td", null, selectedCMRCollection["concept-id"])),
React.createElement("tr", { key: "name" },
React.createElement("td", null, "Name"),
React.createElement("td", null, selectedCMRCollection["ShortName"])),
React.createElement("tr", { key: "description" },
React.createElement("td", null, "Description"),
React.createElement("td", null, selectedCMRCollection["Description"])),
React.createElement("tr", { key: "science-keywords" },
React.createElement("td", null, "Science Keywords"),
parseScienceKeywords(selectedCMRCollection["ScienceKeywords"]).map((item) => item + ", "))) : null,
switchIsDisabled ? React.createElement(AlertBox, { text: "CMR publication not available for selected algorithm.", variant: "primary" }) : null)) : null,
React.createElement("hr", null),
React.createElement(ButtonToolbar, null,
React.createElement(Button, { type: "submit", onClick: () => setShowWaitCursor(true) }, "Submit Job"),
React.createElement(Button, { variant: "outline-secondary", onClick: clearForm }, "Clear"),
React.createElement(Button, { variant: "outline-primary", style: { marginLeft: 'auto' }, onClick: buildNotebookCommand }, "Copy as Jupyter Notebook Code")),
React.createElement("br", null),
React.createElement("p", { id: SUBMITTED_JOB_ELEMENT_ID }))));
};