diff --git a/CHANGELOG.md b/CHANGELOG.md index c998e3d..005c37e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.0] - 2023-12-13 +- Updated /job/new endpoint so it lists the applications for which jobs may be executed. +- Moved chirp rebinning job form to /jobs/new/chirp +- Added job submission forms for L1A and L1B PGEs +- Added process utility to help facilitate changes listed above. + ## [0.3.1] - 2023-12-12 - Fixed link associated with logo on mobile platforms diff --git a/package.json b/package.json index 4e80167..5cf02d5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unity-ui", "private": true, - "version": "0.3.1", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/Root.tsx b/src/Root.tsx index b63c472..b534c06 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -6,12 +6,19 @@ import { import Home from "./routes/home" import JobMonitoring from "./routes/jobs/monitoring"; import NewJob from "./routes/jobs/new"; + import Navbar from "./components/Navbar" import WebView from "./components/WebView"; import Config from "./Config"; +import { getProcesses, getProcessRoute } from "./utils/processes"; +import NotFound from "./routes/errors/not-found"; + function Root() { + + const processes = getProcesses(); + return (
@@ -21,7 +28,20 @@ function Root() { } /> } /> } /> - } /> + + { + /* Add routes for job execution forms */ + processes.map( (item) => { + const path = "/jobs/new/" + item['id']; + const route:JSX.Element | null = getProcessRoute(item['id']); + return ( + } key={"route_" + item['id']}/> + ) + }) + } + + } /> + } />
diff --git a/src/components/BackLink/index.tsx b/src/components/BackLink/index.tsx new file mode 100644 index 0000000..791b544 --- /dev/null +++ b/src/components/BackLink/index.tsx @@ -0,0 +1,18 @@ +import { Link } from "react-router-dom"; +import ChevronLeft from "@nasa-jpl/stellar/icons/chevron_left.svg"; + +type BackLinkProps = { + label:string; + path:string; +}; + +export const BackLink = (props:BackLinkProps) => { + + const {label, path} = props; + + return( + <> + {label}{label} + + ) +} \ No newline at end of file diff --git a/src/components/DocumentMeta/DocumentMeta.tsx b/src/components/DocumentMeta/DocumentMeta.tsx index 7e7d4bb..3be67a9 100644 --- a/src/components/DocumentMeta/DocumentMeta.tsx +++ b/src/components/DocumentMeta/DocumentMeta.tsx @@ -7,7 +7,7 @@ export type DocumentMetaProps = { export const DocumentMeta = (props:DocumentMetaProps) => { - let {title, description} = props; + const {title, description} = props; return ( diff --git a/src/routes/errors/not-found/index.tsx b/src/routes/errors/not-found/index.tsx new file mode 100644 index 0000000..3dc3830 --- /dev/null +++ b/src/routes/errors/not-found/index.tsx @@ -0,0 +1,19 @@ +import { DocumentMeta } from "../../../components/DocumentMeta/DocumentMeta" + +function NotFound() { + + return ( + <> + +
+

Not Found

+ The requested resource cannot be found +
+ + ) +} + +export default NotFound \ No newline at end of file diff --git a/src/routes/jobs/monitoring/index.css b/src/routes/jobs/monitoring/index.css index c037947..88af009 100644 --- a/src/routes/jobs/monitoring/index.css +++ b/src/routes/jobs/monitoring/index.css @@ -8,4 +8,12 @@ .job-detail-item { padding-bottom: 8px; word-wrap: break-word; +} + +.button-bar button { + margin-right: 10px; +} + +.button-bar button:last-child { + margin-right: 0px; } \ No newline at end of file diff --git a/src/routes/jobs/monitoring/index.tsx b/src/routes/jobs/monitoring/index.tsx index 8c94542..1d4c311 100644 --- a/src/routes/jobs/monitoring/index.tsx +++ b/src/routes/jobs/monitoring/index.tsx @@ -155,7 +155,9 @@ function JobMonitoring() {

Job Monitoring

- +
+ +
(); + const [submittingJob, setSubmittingJob] = useState(false); + const tokens = getTokens(); + const meta:{ [key: string]: string} = { + "description": "Create New " + process.title + " Job", + "title": "Create New " + process.title + " Job", + } + + const handleChange = (e:Event & { target: HTMLInputElement}) => { + setForm({ + ...form, + [e.target.id]: e.target.value, + }); + }; + + const handleReset = () => { + setForm(JOB_FORM_INITIAL_STATE); + } + + const setStopDate = () => { + + const endDate = addDays(new Date(form.input_cmr_search_start_time), 16); + + const year = endDate.toLocaleString("default", { year: "numeric" }); + const month = endDate.toLocaleString("default", { month: "2-digit" }); + const day = endDate.toLocaleString("default", { day: "2-digit" }); + const formattedEndDate = year + "-" + month + "-" + day; + + setForm({ + ...form, + ["input_cmr_search_stop_time"]: formattedEndDate + }) + + } + + const addDays = function(date:Date, days:number) { + date.setDate(date.getDate() + days); + return date; + } + + const handleSubmit = async (e:React.FormEvent) => { + + e.preventDefault(); + setSubmittingJob(true); + + const data = { + "mode": "async", + "response": "document", + "inputs": [ + { + "id": "input_processing_labels", + "data": form.input_processing_labels.split(",") + }, + { + "id": "input_cmr_collection_name", + "data": form.input_cmr_collection_name + }, + { + "id": "input_cmr_search_start_time", + "data": form.input_cmr_search_start_time + }, + { + "id": "input_cmr_search_stop_time", + "data": form.input_cmr_search_stop_time + }, + { + "id": "input_cmr_edl_user", + "data": form.input_cmr_edl_user + }, + { + "id": "input_cmr_edl_pass", + "data": form.input_cmr_edl_pass + }, + { + "id": "output_collection_id", + "data": form.output_collection_id + }, + { + "id": "output_data_bucket", + "data": form.output_data_bucket + }, + { + "id": "input_daac_collection_shortname", + "data": form.input_daac_collection_shortname + }, + { + "id": "input_daac_collection_sns", + "data": form.input_daac_collection_sns + } + ], + "outputs": [ + { + "id": "output", + "transmissionMode": "reference" + } + ] + } + + await fetch( + processEndpoint + "/" + process.id + ":" + process.version + "/jobs", + { + method: "POST", + headers: { + "Authorization": "Bearer " + tokens.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + } + ).then( (response:Response) => { + + + if( response.ok ) { + const jobID:string | undefined = response.headers.get("Location")?.replace("http://127.0.0.1:5000/processes/" + process.id.toString() + ":" + process.version.toString() + "/jobs/","") + setNewJobID(jobID); + setSubmittingJob(false); + } + + }).catch( (error:Error) => { + console.debug("Error", error.message); + setSubmittingJob(false); + }) + + handleReset() + + } + + return ( + <> + +
+

{meta["title"]}

+ +
+

Job Parameters

+ { newJobId && + <> +
Your job request was submitted successfully!
+
Your Job ID is {newJobId}
+
+ + } + + + + + + + + +

+ + + + + +
+ + +
+ + +
+ + ) +} + +export default NewJobChirpRebinning; \ No newline at end of file diff --git a/src/routes/jobs/new/index.css b/src/routes/jobs/new/index.css index 4b2130a..4230d12 100644 --- a/src/routes/jobs/new/index.css +++ b/src/routes/jobs/new/index.css @@ -1,5 +1,26 @@ -form { +.app-list-container { width: 600px; background-color: white; padding: 24px; +} + +.app-list { + list-style-type: none; + padding: 0px; + margin: 0px; +} + +.app-list li { + margin-bottom: 20px; +} + +.app-list li:last-child { + margin-bottom: 0px; +} + +.job-form { + background-color: white; + margin-top: 24px; + padding: 4px 24px 24px 24px; + width: 600px; } \ No newline at end of file diff --git a/src/routes/jobs/new/index.tsx b/src/routes/jobs/new/index.tsx index 5d68645..0dd7c71 100644 --- a/src/routes/jobs/new/index.tsx +++ b/src/routes/jobs/new/index.tsx @@ -1,168 +1,15 @@ -import { useState } from "react"; -import { Link } from "react-router-dom"; -import { Button, TextField } from "@nasa-jpl/react-stellar"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@nasa-jpl/react-stellar"; import { DocumentMeta } from "../../../components/DocumentMeta/DocumentMeta"; -import Config from "../../../Config"; -import { getTokens } from '../../../AuthenticationWrapper'; +import { getProcesses } from "../../../utils/processes"; import "./index.css" -const JOB_FORM_PAGE_LOAD_STATE = { - input_processing_labels: "label1, label2", - input_cmr_collection_name: "C2011289787-GES_DISC", - input_cmr_search_start_time: "2016-08-22T00:10:00Z", - input_cmr_search_stop_time: "2016-08-22T01:10:00Z", - input_cmr_edl_user: "cmr_user", - input_cmr_edl_pass: "cmr_pass", - output_collection_id: "urn:nasa:unity:uds_local_test:DEV1:CHRP_16_DAY_REBIN___1", - output_data_bucket: "uds-test-cumulus-sps", - input_daac_collection_shortname: "CHIRP_L1B", - input_daac_collection_sns: "arn:://SNS-arn" -} - -const JOB_FORM_INITIAL_STATE = { - input_processing_labels: "", - input_cmr_collection_name: "", - input_cmr_search_start_time: "", - input_cmr_search_stop_time: "", - input_cmr_edl_user: "cmr_user", - input_cmr_edl_pass: "cmr_pass", - output_collection_id: "", - output_data_bucket: "", - input_daac_collection_shortname: "CHIRP_L1B", - input_daac_collection_sns: "arn:://SNS-arn" -} - function NewJob() { - const processEndpoint = Config['sps']['endpoint'] + 'processes'; - const process:{ id:string, title:string, version:string} = { - id: "chirp", - title: "Chirp Rebinning Workflow", - version: "develop" - } - const [form, setForm] = useState(JOB_FORM_PAGE_LOAD_STATE); - const [newJobId, setNewJobID] = useState(); - const [submittingJob, setSubmittingJob] = useState(false); - const tokens = getTokens(); - - const handleChange = (e:Event & { target: HTMLInputElement}) => { - setForm({ - ...form, - [e.target.id]: e.target.value, - }); - }; - - const handleReset = () => { - setForm(JOB_FORM_INITIAL_STATE); - } - - const setStopDate = () => { - - const endDate = addDays(new Date(form.input_cmr_search_start_time), 16); - - const year = endDate.toLocaleString("default", { year: "numeric" }); - const month = endDate.toLocaleString("default", { month: "2-digit" }); - const day = endDate.toLocaleString("default", { day: "2-digit" }); - const formattedEndDate = year + "-" + month + "-" + day; - - setForm({ - ...form, - ["input_cmr_search_stop_time"]: formattedEndDate - }) - - } - - const addDays = function(date:Date, days:number) { - date.setDate(date.getDate() + days); - return date; - } - - const handleSubmit = async (e:React.FormEvent) => { - - e.preventDefault(); - setSubmittingJob(true); + const navigate = useNavigate(); - const data = { - "mode": "async", - "response": "document", - "inputs": [ - { - "id": "input_processing_labels", - "data": form.input_processing_labels.split(",") - }, - { - "id": "input_cmr_collection_name", - "data": form.input_cmr_collection_name - }, - { - "id": "input_cmr_search_start_time", - "data": form.input_cmr_search_start_time - }, - { - "id": "input_cmr_search_stop_time", - "data": form.input_cmr_search_stop_time - }, - { - "id": "input_cmr_edl_user", - "data": form.input_cmr_edl_user - }, - { - "id": "input_cmr_edl_pass", - "data": form.input_cmr_edl_pass - }, - { - "id": "output_collection_id", - "data": form.output_collection_id - }, - { - "id": "output_data_bucket", - "data": form.output_data_bucket - }, - { - "id": "input_daac_collection_shortname", - "data": form.input_daac_collection_shortname - }, - { - "id": "input_daac_collection_sns", - "data": form.input_daac_collection_sns - } - ], - "outputs": [ - { - "id": "output", - "transmissionMode": "reference" - } - ] - } - - await fetch( - processEndpoint + "/" + process.id + ":" + process.version + "/jobs", - { - method: "POST", - headers: { - "Authorization": "Bearer " + tokens.accessToken, - "Content-Type": "application/json", - }, - body: JSON.stringify(data) - } - ).then( (response:Response) => { - - - if( response.ok ) { - const jobID:string | undefined = response.headers.get("Location")?.replace("http://127.0.0.1:5000/processes/" + process.id.toString() + ":" + process.version.toString() + "/jobs/","") - setNewJobID(jobID); - setSubmittingJob(false); - } - - }).catch( (error:Error) => { - console.debug("Error", error.message); - setSubmittingJob(false); - }) - - handleReset() - - } + const processes = getProcesses(); return ( <> @@ -172,85 +19,18 @@ function NewJob() { />

Create New Job

-
- -

{process.title}

- { newJobId && - <> -
Your job request was submitted successfully!
-
Your Job ID is {newJobId}
-
- - } - - - - - - - - -

- - - - - -
- - -
- - +
+
    + { + processes.map( (item) => { + const path = "/jobs/new/" + item['id']; + return ( +
  • + ) + }) + } +
+
) diff --git a/src/routes/jobs/new/l1a/index.tsx b/src/routes/jobs/new/l1a/index.tsx new file mode 100644 index 0000000..0967ca9 --- /dev/null +++ b/src/routes/jobs/new/l1a/index.tsx @@ -0,0 +1,216 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Button, TextField } from "@nasa-jpl/react-stellar"; +import { DocumentMeta } from "../../../../components/DocumentMeta/DocumentMeta"; +import Config from "../../../../Config"; +import { getTokens } from "../../../../AuthenticationWrapper"; +import { BackLink } from "../../../../components/BackLink"; + +const JOB_FORM_PAGE_LOAD_STATE = { + input_ephatt_collection_id: "", + input_science_collection_id: "", + output_collection_id: "", + static_dir: "", + start_datetime: "", + stop_datetime: "", +} + +const JOB_FORM_INITIAL_STATE = { + input_ephatt_collection_id: "", + input_science_collection_id: "", + output_collection_id: "", + static_dir: "", + start_datetime: "", + stop_datetime: "", +} + +type NewJobL1AProps = { + process:Process +}; + +function NewJobL1A(props:NewJobL1AProps) { + + const processEndpoint = Config['sps']['endpoint'] + 'processes'; + const {process} = props; + const [form, setForm] = useState(JOB_FORM_PAGE_LOAD_STATE); + const [newJobId, setNewJobID] = useState(); + const [submittingJob, setSubmittingJob] = useState(false); + const tokens = getTokens(); + const meta:{ [key: string]: string} = { + "description": "Create New " + process.title + " Job", + "title": "Create New " + process.title + " Job", + } + + const handleChange = (e:Event & { target: HTMLInputElement}) => { + setForm({ + ...form, + [e.target.id]: e.target.value, + }); + }; + + const handleReset = () => { + setForm(JOB_FORM_INITIAL_STATE); + } + + const handleSubmit = async (e:React.FormEvent) => { + + e.preventDefault(); + setSubmittingJob(true); + + const data = { + "mode": "async", + "response": "document", + "inputs": [ + { + "id": "input_ephatt_collection_id", + "data": form.input_ephatt_collection_id + }, + { + "id": "input_science_collection_id", + "data": form.input_science_collection_id + }, + { + "id": "output_collection_id", + "data": form.output_collection_id + }, + { + "id": "static_dir", + "data": form.static_dir + }, + { + "id": "start_datetime", + "data": form.start_datetime + }, + { + "id": "stop_datetime", + "data": form.stop_datetime + }, + ], + "outputs": [ + { + "id": "output", + "transmissionMode": "reference" + } + ] + } + + await fetch( + processEndpoint + "/" + process.id + ":" + process.version + "/jobs", + { + method: "POST", + headers: { + "Authorization": "Bearer " + tokens.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + } + ).then( (response:Response) => { + + + if( response.ok ) { + const jobID:string | undefined = response.headers.get("Location")?.replace("http://127.0.0.1:5000/processes/" + process.id.toString() + ":" + process.version.toString() + "/jobs/","") + setNewJobID(jobID); + setSubmittingJob(false); + } + + }).catch( (error:Error) => { + console.debug("Error", error.message); + setSubmittingJob(false); + }) + + handleReset() + + } + + return ( + <> + +
+

{meta["title"]}

+ +
+

Job Parameters

+ { newJobId && + <> +
Your job request was submitted successfully!
+
Your Job ID is {newJobId}
+
+ + } + + + + + + + + + + + + + + +
+ + +
+ + +
+ + ) +} + +export default NewJobL1A; \ No newline at end of file diff --git a/src/routes/jobs/new/l1b/index.tsx b/src/routes/jobs/new/l1b/index.tsx new file mode 100644 index 0000000..2c68a88 --- /dev/null +++ b/src/routes/jobs/new/l1b/index.tsx @@ -0,0 +1,183 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Button, TextField } from "@nasa-jpl/react-stellar"; +import { DocumentMeta } from "../../../../components/DocumentMeta/DocumentMeta"; +import Config from "../../../../Config"; +import { getTokens } from "../../../../AuthenticationWrapper"; +import { BackLink } from "../../../../components/BackLink"; + +const JOB_FORM_PAGE_LOAD_STATE = { + input_collection_id: "", + start_datetime: "", + stop_datetime: "", + output_collection_id: "", +} + +const JOB_FORM_INITIAL_STATE = { + input_collection_id: "", + start_datetime: "", + stop_datetime: "", + output_collection_id: "", +} + +type NewJobL1BProps = { + process:Process +}; + +function NewJobL1B(props:NewJobL1BProps) { + + const processEndpoint = Config['sps']['endpoint'] + 'processes'; + const {process} = props; + const [form, setForm] = useState(JOB_FORM_PAGE_LOAD_STATE); + const [newJobId, setNewJobID] = useState(); + const [submittingJob, setSubmittingJob] = useState(false); + const tokens = getTokens(); + const meta:{ [key: string]: string} = { + "description": "Create New " + process.title + " Job", + "title": "Create New " + process.title + " Job", + } + + const handleChange = (e:Event & { target: HTMLInputElement}) => { + setForm({ + ...form, + [e.target.id]: e.target.value, + }); + }; + + const handleReset = () => { + setForm(JOB_FORM_INITIAL_STATE); + } + + const handleSubmit = async (e:React.FormEvent) => { + + e.preventDefault(); + setSubmittingJob(true); + + const data = { + "mode": "async", + "response": "document", + "inputs": [ + { + "id": "input_collection_id", + "data": form.input_collection_id + }, + { + "id": "start_datetime", + "data": form.start_datetime + }, + { + "id": "stop_datetime", + "data": form.stop_datetime + }, + { + "id": "output_collection_id", + "data": form.output_collection_id + }, + ], + "outputs": [ + { + "id": "output", + "transmissionMode": "reference" + } + ] + } + + await fetch( + processEndpoint + "/" + process.id + ":" + process.version + "/jobs", + { + method: "POST", + headers: { + "Authorization": "Bearer " + tokens.accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + } + ).then( (response:Response) => { + + + if( response.ok ) { + const jobID:string | undefined = response.headers.get("Location")?.replace("http://127.0.0.1:5000/processes/" + process.id.toString() + ":" + process.version.toString() + "/jobs/","") + setNewJobID(jobID); + setSubmittingJob(false); + } + + }).catch( (error:Error) => { + console.debug("Error", error.message); + setSubmittingJob(false); + }) + + handleReset() + + } + + return ( + <> + +
+

{meta["title"]}

+ +
+

Job Parameters

+ { newJobId && + <> +
Your job request was submitted successfully!
+
Your Job ID is {newJobId}
+
+ + } + + + + + + + + + +
+ + +
+ + +
+ + ) +} + +export default NewJobL1B; \ No newline at end of file diff --git a/src/types/process.d.tsx b/src/types/process.d.tsx new file mode 100644 index 0000000..a68f27a --- /dev/null +++ b/src/types/process.d.tsx @@ -0,0 +1,5 @@ +interface Process { + id:string; + title:string; + version:string; +} \ No newline at end of file diff --git a/src/utils/processes.tsx b/src/utils/processes.tsx new file mode 100644 index 0000000..6ccad44 --- /dev/null +++ b/src/utils/processes.tsx @@ -0,0 +1,49 @@ +import NewJobChirpRebinning from "../routes/jobs/new/chirp"; +import NewJobL1A from "../routes/jobs/new/l1a"; +import NewJobL1B from "../routes/jobs/new/l1b"; + +const getProcesses = ():Process[] => { + return ( + [ + { + id: "chirp", + title: "CHIRP Rebinning", + version: "develop", + }, + { + id: "l1a", + title: "l1a_pge_cwl", + version: "develop", + }, + { + id: "l1b", + title: "l1b_pge_cwl", + version: "develop", + }, + ] + ) +}; + +const getProcess = (processID:string):Process => { + const processes:Process[] = getProcesses(); + const index = processes.findIndex( process => process.id === processID ); + return processes[index]; +}; + + +const getProcessRoute = (processID:string):JSX.Element | null => { + + const process:Process = getProcess(processID) + const processRoutes:{ [key: string]: JSX.Element} = { + "chirp": , + "l1a": , + "l1b": , + } + + return ( + (process) ? processRoutes[processID] : null + ) + +}; + +export { getProcesses, getProcess, getProcessRoute }; \ No newline at end of file