forked from reanahub/reana-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
launch: add launcher URL and badge creator page
Closes reanahub#324.
- Loading branch information
1 parent
6ccca0e
commit e600a37
Showing
6 changed files
with
341 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
297 changes: 297 additions & 0 deletions
297
reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
/* | ||
This file is part of REANA. | ||
Copyright (C) 2023 CERN. | ||
REANA is free software; you can redistribute it and/or modify it | ||
under the terms of the MIT License; see LICENSE file for more details. | ||
*/ | ||
|
||
import { useState } from "react"; | ||
import { | ||
Button, | ||
Container, | ||
Divider, | ||
Form, | ||
Icon, | ||
Popup, | ||
Segment, | ||
} from "semantic-ui-react"; | ||
|
||
import styles from "./LauncherBadgeCreator.module.scss"; | ||
import { CodeSnippet, Title } from "~/components"; | ||
import { api, LAUNCH_ON_REANA_BADGE_URL } from "~/config"; | ||
import BasePage from "~/pages/BasePage"; | ||
import { triggerNotification } from "~/actions"; | ||
import { useDispatch } from "react-redux"; | ||
import { stringifyQueryParams } from "~/util"; | ||
import { Link } from "react-router-dom"; | ||
|
||
const LauncherBadgeCreator = () => { | ||
const dispatch = useDispatch(); | ||
const [launcherData, setLauncherData] = useState({ | ||
url: "", | ||
analysisName: "", | ||
specFileName: "reana.yaml", | ||
parameters: [], | ||
}); | ||
|
||
const [validationErrors, setValidationErrors] = useState({ | ||
url: null, | ||
analysisName: null, | ||
specFileName: null, | ||
parameters: {}, | ||
}); | ||
|
||
const [launcherURL, setLauncherURL] = useState(); | ||
|
||
const handleInputChange = (e, { name, value }) => { | ||
// Clear the validation errors relative to the field that has been changed | ||
setValidationErrors((prevErrors) => ({ ...prevErrors, [name]: null })); | ||
setLauncherData({ ...launcherData, [name]: value }); | ||
}; | ||
|
||
const handleAddParam = () => { | ||
setLauncherData((prevData) => ({ | ||
...prevData, | ||
parameters: [...prevData.parameters, { key: "", value: "" }], | ||
})); | ||
}; | ||
|
||
const handleDeleteParam = (index, paramKey) => { | ||
// Clear the validation error relative to the parameter that has been deleted | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
parameters: { ...prevErrors.parameters, [paramKey]: null }, | ||
})); | ||
setLauncherData((prevData) => { | ||
const newParameters = [...prevData.parameters]; | ||
newParameters.splice(index, 1); | ||
return { ...prevData, parameters: newParameters }; | ||
}); | ||
}; | ||
|
||
const handleParamChange = (index, key, value, paramKey) => { | ||
// Clear the validation errors relative to the parameter that has been changed | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
parameters: { ...prevErrors.parameters, [paramKey]: null }, | ||
})); | ||
setLauncherData((prevData) => { | ||
const newParameters = [...prevData.parameters]; | ||
newParameters[index] = { ...newParameters[index], [key]: value }; | ||
return { ...prevData, parameters: newParameters }; | ||
}); | ||
}; | ||
|
||
const handleSubmit = () => { | ||
let errorMessage = null; | ||
let paramsString = null; | ||
// Validate URL | ||
try { | ||
new URL(launcherData.url); | ||
} catch (e) { | ||
errorMessage = "The form contains invalid data!"; | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
url: "Not a valid URL!", | ||
})); | ||
} | ||
|
||
// Validate analysis name | ||
if (launcherData.analysisName) { | ||
if (launcherData.analysisName.includes(".")) { | ||
errorMessage = "The form contains invalid data!"; | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
analysisName: "The analysis name cannot contain dots!", | ||
})); | ||
} | ||
} | ||
|
||
// Validate specification file name | ||
if ( | ||
launcherData.specFileName && | ||
launcherData.specFileName !== "reana.yaml" | ||
) { | ||
if ( | ||
!launcherData.specFileName.endsWith(".yaml") && | ||
!launcherData.specFileName.endsWith(".yml") | ||
) { | ||
errorMessage = "The form contains invalid data!"; | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
specFileName: | ||
"The specification file name must end with .yaml or .yml", | ||
})); | ||
} | ||
} | ||
|
||
if (launcherData.parameters.length > 0) { | ||
// The value of each parameter is considered to be a JSON-encoded string. | ||
// In this way, the user can pass integers, lists, complex objects, etc. | ||
|
||
// Validate the value of each parameter (valid JSON string) | ||
let paramsObject = {}; | ||
launcherData.parameters.forEach((param, index) => { | ||
try { | ||
paramsObject[param.key] = JSON.parse(param.value); | ||
} catch (e) { | ||
errorMessage = "The form contains invalid data!"; | ||
setValidationErrors((prevErrors) => ({ | ||
...prevErrors, | ||
parameters: { | ||
...prevErrors.parameters, | ||
[param.key]: "Not a valid JSON string!", | ||
}, | ||
})); | ||
} | ||
paramsString = JSON.stringify(paramsObject); | ||
}); | ||
} | ||
|
||
// Display an error message if the form data is invalid | ||
if (errorMessage) { | ||
dispatch( | ||
triggerNotification(errorMessage, "", { | ||
error: true, | ||
}), | ||
); | ||
setLauncherURL(null); | ||
return; | ||
} | ||
setLauncherURL( | ||
`${api}/launch?${stringifyQueryParams({ | ||
url: launcherData.url, | ||
name: launcherData.analysisName, | ||
specification: launcherData.specFileName, | ||
parameters: paramsString, | ||
})}`, | ||
); | ||
}; | ||
|
||
return ( | ||
<BasePage title="Launcher badge creator"> | ||
<Container text className={styles["container"]}> | ||
<Title>Create your own "Launch on REANA" badge</Title> | ||
<p> | ||
Fill in the form below to generate the URL to the REANA launcher that | ||
will run your analysis, as well as the markdown code for a "Launch on | ||
REANA" badge that you can include in your GitHub repositories! | ||
</p> | ||
<Segment className={styles.segment}> | ||
<Form onSubmit={handleSubmit}> | ||
<Form.Group widths="equal"> | ||
<Form.Input | ||
id="url" | ||
label="URL of the workflow repository" | ||
name="url" | ||
onChange={handleInputChange} | ||
required | ||
error={validationErrors.url} | ||
fluid | ||
/> | ||
</Form.Group> | ||
<Form.Group widths="equal"> | ||
<Form.Input | ||
id="analysisName" | ||
label="Analysis name" | ||
name="analysisName" | ||
onChange={handleInputChange} | ||
error={validationErrors.analysisName} | ||
fluid | ||
/> | ||
<Form.Input | ||
id="specFileName" | ||
label="REANA specification filename" | ||
placeholder="reana.yaml" | ||
name="specFileName" | ||
onChange={handleInputChange} | ||
error={validationErrors.specFileName} | ||
fluid | ||
/> | ||
</Form.Group> | ||
<div className={styles.abc}> | ||
<label className={styles.label}> | ||
Parameters{" "} | ||
<Popup | ||
trigger={<Icon name="info circle" />} | ||
content={ | ||
"For each parameter, insert the value as a JSON-encoded string. " + | ||
"For example, if you want to pass an integer, you can write 1, " + | ||
'but if you want to pass a list of strings, you need to write ["a", "b", "c"].' | ||
} | ||
/> | ||
</label> | ||
<Button type="button" onClick={handleAddParam}> | ||
<Icon name="plus" /> Add Parameter | ||
</Button> | ||
</div> | ||
{launcherData.parameters.map((param, index) => ( | ||
<Form.Group key={index} className={styles["parameter-row"]}> | ||
<Form.Input | ||
id={`key-${index}`} | ||
width={5} | ||
placeholder="Key" | ||
name="key" | ||
value={param.key} | ||
onChange={(e) => | ||
handleParamChange(index, "key", e.target.value) | ||
} | ||
/> | ||
<Form.Input | ||
id={`value-${index}`} | ||
width={10} | ||
placeholder="Value" | ||
name="value" | ||
value={param.value} | ||
onChange={(e) => | ||
handleParamChange(index, "value", e.target.value, param.key) | ||
} | ||
error={ | ||
validationErrors.parameters[ | ||
launcherData.parameters[index].key | ||
] | ||
} | ||
/> | ||
<Form.Button | ||
type="button" | ||
width={1} | ||
icon="delete" | ||
onClick={() => handleDeleteParam(index, param.key)} | ||
/> | ||
</Form.Group> | ||
))} | ||
<Button primary fluid type="submit"> | ||
Create badge! | ||
</Button> | ||
</Form> | ||
{!!launcherURL && ( | ||
<> | ||
<Divider></Divider> | ||
<p> | ||
Here you can find the URL you can use to launch the workflow: | ||
</p> | ||
<CodeSnippet dollarPrefix={false} copy> | ||
<div>{launcherURL}</div> | ||
</CodeSnippet> | ||
<p> | ||
And here is the Markdown code for your badge: | ||
<Link to={launcherURL}> | ||
<img src={LAUNCH_ON_REANA_BADGE_URL} alt="Launch on REANA" /> | ||
</Link> | ||
</p> | ||
<CodeSnippet dollarPrefix={false} copy> | ||
<div> | ||
[![Launch on REANA]({LAUNCH_ON_REANA_BADGE_URL})]($ | ||
{launcherURL}) | ||
</div> | ||
</CodeSnippet> | ||
</> | ||
)} | ||
</Segment> | ||
</Container> | ||
</BasePage> | ||
); | ||
}; | ||
|
||
export default LauncherBadgeCreator; |
23 changes: 23 additions & 0 deletions
23
reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* | ||
This file is part of REANA. | ||
Copyright (C) 2023 CERN. | ||
REANA is free software; you can redistribute it and/or modify it | ||
under the terms of the MIT License; see LICENSE file for more details. | ||
*/ | ||
|
||
.container { | ||
margin-top: 5em; | ||
} | ||
|
||
.segment { | ||
margin-bottom: 1em !important; | ||
} | ||
|
||
.abc { | ||
margin-bottom: 1rem; | ||
} | ||
|
||
.parameter-row { | ||
box-sizing: content-box; | ||
} |
Oops, something went wrong.