Skip to content

Commit

Permalink
launch: add launcher URL and badge creator page
Browse files Browse the repository at this point in the history
Closes reanahub#324.
  • Loading branch information
giuseppe-steduto committed Nov 7, 2023
1 parent 6ccca0e commit e600a37
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changes
Version 0.9.2 (UNRELEASED)
--------------------------

- Adds form to generate launcher URL and markdown badge to launch an analysis on REANA.
- Adds option to delete all the runs of a workflow.
- Changes the launch on REANA page to display the optional launch parameters in a table.
- Changes Docker image Node version from 16 to 18.
Expand Down
12 changes: 11 additions & 1 deletion reana-ui/src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020, 2022 CERN.
Copyright (C) 2020, 2022, 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.
Expand Down Expand Up @@ -40,6 +40,7 @@ import NotFound from "~/pages/error/NotFound";
import Error from "./Error";

import "./App.module.scss";
import LauncherBadgeCreator from "~/pages/badgeCreator/LauncherBadgeCreator";

function RequireAuth({ children }) {
const signedIn = useSelector(isSignedIn);
Expand Down Expand Up @@ -124,6 +125,15 @@ export default function App() {
</RequireAuth>
}
/>
<Route
exact
path="/launcher-badge"
element={
<RequireAuth>
<LauncherBadgeCreator />
</RequireAuth>
}
/>
<Route
path="/"
element={
Expand Down
297 changes: 297 additions & 0 deletions reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.js
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 reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss
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;
}
Loading

0 comments on commit e600a37

Please sign in to comment.