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}
+ >
+ )
+}
\ 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
- navigate("/jobs/new")}>Run New Job or Batch
+
+ navigate("/jobs/new")}>Run New Job
+
();
+ 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 (
+ <>
+
+
+ >
+ )
+}
+
+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
-
+
+
+ {
+ processes.map( (item) => {
+ const path = "/jobs/new/" + item['id'];
+ return (
+ navigate(path)}>Run New {item['title']} Job
+ )
+ })
+ }
+
+
>
)
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 (
+ <>
+
+
+ >
+ )
+}
+
+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 (
+ <>
+
+
+ >
+ )
+}
+
+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