UNPKG

@maap-jupyterlab/dps-jupyter-extension

Version:

A JupyterLab extension for submitting and viewing jobs.

297 lines (296 loc) 15.6 kB
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 })))); };