CEC-5436: add release_notes, update_duration, and max_attempts to manifest update form (#489)

This commit is contained in:
Tristan Timblin
2023-12-01 11:41:27 -08:00
committed by GitHub
parent 5ad467b116
commit 3c19a8d601
5 changed files with 266 additions and 10 deletions

View File

@@ -0,0 +1,44 @@
import {
Box,
FormControl,
FormHelperText,
Input,
InputLabel,
} from "@material-ui/core";
const PREFIX = "number-field";
function kebab(str) {
return str.replaceAll(" ", "-").toLowerCase();
}
export function NumberField({
name,
description,
value = 0,
setValue = () => { },
...rest
}) {
const inputId = `${PREFIX}-${kebab(name)}`;
const describeId = `${PREFIX}-${kebab(name)}-describe`;
const handleChange = (event) => {
setValue(event.target.value);
}
return (
<Box sx={{ my: 2 }}>
<FormControl fullWidth>
<InputLabel htmlFor={inputId}>{name}</InputLabel>
<Input
id={inputId}
aria-describedby={describeId}
type="number"
value={value}
onChange={handleChange}
{...rest}
/>
<FormHelperText id={describeId}>{description}</FormHelperText>
</FormControl>
</Box>
);
}

View File

@@ -0,0 +1,55 @@
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import { NumberField } from "./index";
describe("NumberField", () => {
it("renders with form label and aria describe", () => {
const { getByText } = render(
<NumberField
name="My First Field"
description="Some helper text"
/>
);
const labelEl = getByText("My First Field");
const describeEl = getByText("Some helper text");
expect(labelEl.htmlFor).toEqual("number-field-my-first-field");
expect(describeEl.id).toEqual("number-field-my-first-field-describe");
});
it("input is of type number", () => {
const { getByLabelText } = render(
<NumberField
name="My First Field"
description="Some helper text"
/>
);
const inputEl = getByLabelText("My First Field", { selector: "input" });
expect(inputEl.type).toEqual("number");
});
it("updates parent state", () => {
let mockState = 0;
const mockSetState = jest.fn((value) => mockState = value);
const { getByLabelText } = render(
<NumberField
name="My First Field"
description="Enter a number"
value={mockState}
setValue={mockSetState}
/>
);
const inputEl = getByLabelText("My First Field", { selector: "input" });
fireEvent.change(inputEl, { target: { value: "1" } });
expect(mockState).toEqual("1");
expect(mockSetState.mock.calls.length).toEqual(1);
});
})

View File

@@ -0,0 +1 @@
export * from "./NumberField";

View File

@@ -112,6 +112,43 @@ exports[`Manifest Details Component Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true"
for="release_notes"
id="release_notes-label"
>
Release Notes
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="release_notes"
maxlength="255"
name="release_notes"
type="text"
value="https://releasenotes.com"
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-0 PrivateNotchedOutline-legendNotched-0"
>
<span>
Release Notes
</span>
</legend>
</fieldset>
</div>
</div>
<div <div
class="MuiFormControl-root MuiFormControl-marginNormal" class="MuiFormControl-root MuiFormControl-marginNormal"
> >
@@ -334,6 +371,68 @@ exports[`Manifest Details Component Render 1`] = `
</fieldset> </fieldset>
</div> </div>
</div> </div>
<div
class="MuiBox-root MuiBox-root-0"
>
<div
class="MuiFormControl-root MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled"
data-shrink="true"
for="number-field-update-duration"
>
Update Duration
</label>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl"
>
<input
aria-describedby="number-field-update-duration-describe"
aria-invalid="false"
class="MuiInputBase-input MuiInput-input"
id="number-field-update-duration"
type="number"
value="0"
/>
</div>
<p
class="MuiFormHelperText-root MuiFormHelperText-filled"
id="number-field-update-duration-describe"
/>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-0"
>
<div
class="MuiFormControl-root MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled"
data-shrink="true"
for="number-field-max-attempts"
>
Max Attempts
</label>
<div
class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl"
>
<input
aria-describedby="number-field-max-attempts-describe"
aria-invalid="false"
class="MuiInputBase-input MuiInput-input"
id="number-field-max-attempts"
type="number"
value="0"
/>
</div>
<p
class="MuiFormHelperText-root MuiFormHelperText-filled"
id="number-field-max-attempts-describe"
/>
</div>
</div>
<button <button
aria-label="send command" aria-label="send command"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth" class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"

View File

@@ -10,6 +10,7 @@ import { DropDownList } from "../../Controls/DropDownList";
import { RoleWrap } from "../../Controls/RoleWrap"; import { RoleWrap } from "../../Controls/RoleWrap";
import { Permissions } from "../../../utils/roles"; import { Permissions } from "../../../utils/roles";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import { NumberField } from "../../Controls/NumberField";
const manifestTypes = [ const manifestTypes = [
{ value: "standard", label: "Standard" }, { value: "standard", label: "Standard" },
@@ -17,13 +18,13 @@ const manifestTypes = [
]; ];
const activeStates = [ const activeStates = [
{value: true, label: "Active" }, { value: true, label: "Active" },
{value: false, label: "Archived" }, { value: false, label: "Archived" },
]; ];
const booleanStates = [ const booleanStates = [
{value: true, label: "True" }, { value: true, label: "True" },
{value: false, label: "False" }, { value: false, label: "False" },
]; ];
const emptyManifest = { const emptyManifest = {
@@ -51,15 +52,22 @@ const MainForm = () => {
const { setMessage, setTitle, setSitePath } = useStatusContext(); const { setMessage, setTitle, setSitePath } = useStatusContext();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [releaseNotes, setReleaseNotes] = useState("");
const [type, setType] = useState(""); const [type, setType] = useState("");
const [active, setActive] = useState(true); // So !active = archived const [active, setActive] = useState(true); // So !active = archived
const [rollback, setRollback] = useState(true); const [rollback, setRollback] = useState(true);
const [env, setEnv] = useState("current"); const [env, setEnv] = useState("current");
const [updateDuration, setUpdateDuration] = useState(0);
const [maxAttempts, setMaxAttempts] = useState(0);
const changeName = (e) => { const changeName = (e) => {
setName(e.target.value); setName(e.target.value);
} }
const changeReleaseNotes = (e) => {
setReleaseNotes(e.target.value);
}
const changeType = (e) => { const changeType = (e) => {
setType(e.target.value); setType(e.target.value);
}; };
@@ -76,10 +84,33 @@ const MainForm = () => {
setEnv(e.target.value) setEnv(e.target.value)
} }
const changeUpdateDuration = (value) => {
if (value < 0) {
return;
}
setUpdateDuration(value);
}
const changeMaxAttempts = (value) => {
if (value < 0) {
return;
}
setMaxAttempts(value);
}
const onSubmit = async (e) => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const result = await updateManifest(manifest_id, { name, type, active, rollback, env }, token); const result = await updateManifest(manifest_id, {
name,
release_notes: releaseNotes,
type,
active,
rollback,
env,
update_duration: parseInt(updateDuration),
max_attempts: parseInt(maxAttempts),
}, token);
if (!result || result.error) return; if (!result || result.error) return;
setMessage(`Updated manifest ${manifest_id}`); setMessage(`Updated manifest ${manifest_id}`);
@@ -92,11 +123,11 @@ const MainForm = () => {
const manifestMigrate = async (e) => { const manifestMigrate = async (e) => {
e.preventDefault(); e.preventDefault();
try{ try {
const result = await migrateManifest(manifest_id, token) const result = await migrateManifest(manifest_id, token)
if (!result || result.error) return; if (!result || result.error) return;
setMessage(`Manifest Migrated ${manifest_id}`) setMessage(`Manifest Migrated ${manifest_id}`)
} catch(e) { } catch (e) {
setMessage(`Failed to update manifest ${manifest_id}`) setMessage(`Failed to update manifest ${manifest_id}`)
} }
} }
@@ -110,10 +141,13 @@ const MainForm = () => {
} else { } else {
setManifest(result); setManifest(result);
setName(result.name); setName(result.name);
setReleaseNotes(result.release_notes);
setType(result.type); setType(result.type);
setActive(result.active); setActive(result.active);
setRollback(result.rollback); setRollback(result.rollback);
setEnv(result.env ?? "current"); setEnv(result.env ?? "current");
setUpdateDuration(result.update_duration);
setMaxAttempts(result.max_attempts);
} }
} catch (e) { } catch (e) {
setMessage(e.message); setMessage(e.message);
@@ -176,12 +210,35 @@ const MainForm = () => {
fullWidth fullWidth
onChange={changeName} onChange={changeName}
/> />
<TextField
id="release_notes"
name="release_notes"
label="Release Notes"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "255",
}}
value={releaseNotes}
fullWidth
onChange={changeReleaseNotes}
/>
<DropDownList label="Type" data={manifestTypes} classes={classes} onChange={changeType} value={type} /> <DropDownList label="Type" data={manifestTypes} classes={classes} onChange={changeType} value={type} />
<DropDownList label="Active" data={activeStates} classes={classes} onChange={changeActive} value={active}/> <DropDownList label="Active" data={activeStates} classes={classes} onChange={changeActive} value={active} />
<DropDownList label="Rollback" data={booleanStates} classes={classes} onChange={changeRollback} value={rollback}/> <DropDownList label="Rollback" data={booleanStates} classes={classes} onChange={changeRollback} value={rollback} />
{ {
ENVS.length > 1 && <DropDownList label="ECC Keys" data={ENVS} classes={classes} onChange={changeEnv} value={env}/> ENVS.length > 1 && <DropDownList label="ECC Keys" data={ENVS} classes={classes} onChange={changeEnv} value={env} />
} }
<NumberField
name="Update Duration"
value={updateDuration}
setValue={changeUpdateDuration}
/>
<NumberField
name="Max Attempts"
value={maxAttempts}
setValue={changeMaxAttempts}
/>
<Button <Button
type="submit" type="submit"
aria-label="send command" aria-label="send command"