From e600a3765a67899e209125a933e4470c49a888c6 Mon Sep 17 00:00:00 2001 From: Giuseppe Steduto Date: Tue, 24 Oct 2023 18:09:11 +0200 Subject: [PATCH] launch: add launcher URL and badge creator page Closes #324. --- CHANGES.rst | 1 + reana-ui/src/components/App.js | 12 +- .../badgeCreator/LauncherBadgeCreator.js | 297 ++++++++++++++++++ .../LauncherBadgeCreator.module.scss | 23 ++ .../src/pages/launchOnReana/LaunchOnReana.js | 38 +-- reana-ui/src/pages/launchOnReana/Welcome.js | 5 + 6 files changed, 341 insertions(+), 35 deletions(-) create mode 100644 reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.js create mode 100644 reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss diff --git a/CHANGES.rst b/CHANGES.rst index 743c8901..ad65325f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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. diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index 67064333..5b2fad20 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -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. @@ -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); @@ -124,6 +125,15 @@ export default function App() { } /> + + + + } + /> { + 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 ( + + + Create your own "Launch on REANA" badge +

+ 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! +

+ +
+ + + + + + + +
+ + +
+ {launcherData.parameters.map((param, index) => ( + + + handleParamChange(index, "key", e.target.value) + } + /> + + handleParamChange(index, "value", e.target.value, param.key) + } + error={ + validationErrors.parameters[ + launcherData.parameters[index].key + ] + } + /> + handleDeleteParam(index, param.key)} + /> + + ))} + +
+ {!!launcherURL && ( + <> + +

+ Here you can find the URL you can use to launch the workflow: +

+ +
{launcherURL}
+
+

+ And here is the Markdown code for your badge: + + Launch on REANA + +

+ +
+ [![Launch on REANA]({LAUNCH_ON_REANA_BADGE_URL})]($ + {launcherURL}) +
+
+ + )} +
+
+
+ ); +}; + +export default LauncherBadgeCreator; diff --git a/reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss b/reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss new file mode 100644 index 00000000..4a60effd --- /dev/null +++ b/reana-ui/src/pages/badgeCreator/LauncherBadgeCreator.module.scss @@ -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; +} diff --git a/reana-ui/src/pages/launchOnReana/LaunchOnReana.js b/reana-ui/src/pages/launchOnReana/LaunchOnReana.js index c569f6f5..8a7d391e 100644 --- a/reana-ui/src/pages/launchOnReana/LaunchOnReana.js +++ b/reana-ui/src/pages/launchOnReana/LaunchOnReana.js @@ -9,29 +9,15 @@ import { useState, useMemo } from "react"; import { Navigate, useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; -import { - Button, - Container, - Icon, - Image, - Loader, - Table, -} from "semantic-ui-react"; +import { Button, Container, Icon, Loader, Table } from "semantic-ui-react"; import BasePage from "../BasePage"; import Welcome from "./Welcome"; import { Box, CodeSnippet, Title } from "~/components"; -import { - clearNotification, - errorActionCreator, - triggerNotification, -} from "~/actions"; +import { errorActionCreator, triggerNotification } from "~/actions"; import client from "~/client"; import { useQuery } from "~/hooks"; -import { - LAUNCH_ON_REANA_PARAMS_WHITELIST, - LAUNCH_ON_REANA_BADGE_URL, -} from "~/config"; +import { LAUNCH_ON_REANA_PARAMS_WHITELIST } from "~/config"; import { getReanaToken } from "~/selectors"; import styles from "./LaunchOnReana.module.scss"; @@ -232,7 +218,7 @@ export default function LaunchOnReana() { - ) + ), )} @@ -251,19 +237,3 @@ export default function LaunchOnReana() { ); } - -const BadgeEmbed = () => ( -
- - Expand to see the text below, paste it into your README to show a REANA - badge:{" "} - - - -
- [![Launch on REANA]({LAUNCH_ON_REANA_BADGE_URL})]($ - {window.location.href}) -
-
-
-); diff --git a/reana-ui/src/pages/launchOnReana/Welcome.js b/reana-ui/src/pages/launchOnReana/Welcome.js index 94e97576..591b03bd 100644 --- a/reana-ui/src/pages/launchOnReana/Welcome.js +++ b/reana-ui/src/pages/launchOnReana/Welcome.js @@ -68,6 +68,11 @@ export default function Welcome() { ))} +

+ If you have your own analysis, you can create a badge to quickly launch + it on REANA! Visit this page to get + started. +

); }