Skip to content

Commit

Permalink
feat(client): support template variables metadata (#1579)
Browse files Browse the repository at this point in the history
* Style and show input elements based on the variable type.
* Automatically set default values.

fix #1320
  • Loading branch information
lorenzo-cavazzi committed Jan 3, 2022
1 parent 0311461 commit 2d29997
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 17 deletions.
158 changes: 148 additions & 10 deletions client/src/project/new/ProjectNew.present.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faExclamationTriangle, faInfoCircle, faLink, faQuestionCircle, faSyncAlt
faExclamationTriangle, faInfoCircle, faLink, faQuestionCircle, faSyncAlt, faUndo
} from "@fortawesome/free-solid-svg-icons";

import {
Expand Down Expand Up @@ -974,6 +974,32 @@ function TemplateGalleryRow(props) {
);
}

/**
* Create a "restore default" button.
*
* @param {function} restore - function to invoke
* @param {string} tip - message to display in the tooltip
* @param {boolean} disabled - whether it's disabled or not
*/
function RestoreButton(props) {
const { restore, name, disabled } = props;

const id = `restore_${name}`;
const tip = disabled ?
"Default value already selected" :
"Restore default value";

return (
<div id={id} className="d-inline ms-2">
<Button key="button" className="p-0" color="link" size="sm"
onClick={() => restore()} disabled={disabled} >
<FontAwesomeIcon icon={faUndo} />
</Button>
<UncontrolledTooltip key="tooltip" placement="top" target={id}>{tip}</UncontrolledTooltip>
</div>
);
}

class Variables extends Component {
render() {
const { input, handlers } = this.props;
Expand All @@ -987,19 +1013,111 @@ class Variables extends Component {
const template = templates.all.filter(t => t.id === input.template)[0];
if (!template || !template.variables || !Object.keys(template.variables).length)
return null;
const variables = Object.keys(template.variables).map(variable => (
<FormGroup key={variable}>
<Label>{capitalize(variable)}</Label>
<Input id={"parameter-" + variable} type="text" value={input.variables[variable]}
onChange={(e) => handlers.setVariable(variable, e.target.value)} />
<FormText>{capitalize(template.variables[variable])}</FormText>
</FormGroup>
));
const variables = Object.keys(template.variables).map(variable => {
const data = template.variables[variable];

// fallback to avoid breaking old variable structure
if (typeof data !== "object") {
return (
<FormGroup key={variable}>
<Label>{capitalize(variable)}</Label>
<Input id={"parameter-" + variable} type="text" value={input.variables[variable]}
onChange={(e) => handlers.setVariable(variable, e.target.value)} />
<FormText>{capitalize(template.variables[variable])}</FormText>
</FormGroup>
);
}

// expected `data` properties: default_value, description, enum, type.
// changing enum to enumValues to avoid using js reserved word
return (
<Variable
enumValues={data["enum"]}
handlers={handlers}
key={variable}
input={input}
name={variable}
{...data}
/>
);
});

return variables;
}
}

function Variable(props) {
const { default_value, description, enumValues, handlers, input, name, type } = props;
const id = `parameter-${name}`;

const descriptionOutput = description ?
(<FormText>{capitalize(description)}</FormText>) :
null;

const defaultOutput = default_value != null ?
`Default: ${default_value}` :
null;

const restoreButton = default_value != null ?
(
<RestoreButton
disabled={input.variables[name] === default_value}
name={name}
restore={() => handlers.setVariable(name, default_value)}
/>
) :
null;

let inputElement = null;
if (type === "boolean") {
inputElement = (
<FormGroup className="form-check form-switch d-inline-block">
<Input type="switch" id={id} label={name}
checked={input.variables[name]}
onChange={(e) => handlers.setVariable(name, e.target.checked)}
className="form-check-input rounded-pill" />
<Label check htmlFor={"parameter-" + name}>{name}</Label>
{restoreButton}
</FormGroup>
);
// inputElement = null;
}
else if (type === "enum") {
const enumObjects = enumValues.map(enumObject => {
const enumId = `enum-${id}-${enumObject.toString()}`;
return (
<option key={enumId} value={enumObject}>{enumObject}</option>
);
});
inputElement = (
<FormGroup>
<Label>{name}</Label>{restoreButton}
<Input id={id} type="select" value={input.variables[name]}
onChange={(e) => handlers.setVariable(name, e.target.value)}>
{enumObjects}
</Input>
{descriptionOutput}
</FormGroup>
);
}
else {
const inputType = type === "number" ?
"number" :
"text";
inputElement = (
<FormGroup>
<Label>{name}</Label>{restoreButton}
<Input id={id} type={inputType} value={input.variables[name]}
onChange={(e) => handlers.setVariable(name, e.target.value)}
placeholder={defaultOutput} />
{descriptionOutput}
</FormGroup>
);
}

return inputElement;
}

class Create extends Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -1109,6 +1227,25 @@ class Create extends Component {
"based on " + (templates.all.find(t => t.id === input.template).name) :
"";

const errorFields = meta.validation.errors ?
Object.keys(meta.validation.errors)
.filter(field => !input[`${field}Pristine`]) // don't consider pristine fields
.map(field => capitalize(field)) :
[];
const plural = errorFields.length > 1 ?
"s" :
"";
const errorMessage = errorFields.length ?
(
<FormText className="d-block">
<span className="text-danger">
To create a new project, please first fix problems with the following field{plural}:{" "}
<span className="fw-bold">{errorFields.join(", ")}</span>
</span>
</FormText>
) :
null;

return (
<Fragment>
{alert}
Expand All @@ -1124,6 +1261,7 @@ class Create extends Component {
meta={meta}
createUrl={this.props.handlers.createEncodedUrl}
/>
{errorMessage}
</Fragment>
);
}
Expand Down Expand Up @@ -1204,7 +1342,7 @@ function ShareLinkModal(props) {
if (include.variables) {
let variablesObject = {};
for (let variable of Object.keys(input.variables)) {
if (input.variables[variable])
if (input.variables[variable] != null)
variablesObject[variable] = input.variables[variable];
}
dataObject.variables = variablesObject;
Expand Down
29 changes: 22 additions & 7 deletions client/src/project/new/ProjectNew.state.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,32 @@ class NewProjectCoordinator {
Object.keys(template.variables) :
[];

// preserve already set values
// preserve already set values or set default values when available
const oldValues = currentInput.template ?
currentInput.variables :
{};
const oldVariables = Object.keys(oldValues);
const values = variables.reduce((values, variable) => {
const text = oldVariables.includes(variable) ?
oldValues[variable] :
"";
return { ...values, [variable]: text };
let value = "";

const variableData = template.variables[variable];
if (typeof variableData === "object") {
// set first value for enum, and "false" for boolean
if (variableData["type"] === "enum" && variableData["enum"] && variableData["enum"].length)
value = variableData["enum"][0];

// set default, if any
if (typeof variableData === "object" && variableData["default_value"] != null)
value = variableData["default_value"];
}

// set older value, if any
if (oldVariables.includes(variable))
value = oldValues[variable];

return { ...values, [variable]: value };
}, {});

return values;
}

Expand Down Expand Up @@ -535,10 +550,10 @@ class NewProjectCoordinator {
newProjectData.ref = userTemplates.ref;
}

// add variables
// add variables after converting to string (renku core accept string only)
let parameters = [];
for (let variable of Object.keys(input.variables))
parameters.push({ key: variable, value: input.variables[variable] });
parameters.push({ key: variable, value: input.variables[variable].toString() });
newProjectData.parameters = parameters;

// reset all previous creation progresses and invoke the project creation API
Expand Down

0 comments on commit 2d29997

Please sign in to comment.