Packs UI: Create pack navigates to pack's edit page (#2581)

This commit is contained in:
RachelElysia 2021-10-25 09:43:52 -04:00 committed by GitHub
parent 3369436741
commit fdfabf9e32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 177 deletions

View file

@ -0,0 +1 @@
* Redirected to edit pack page after creating a pack

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useDeepEffect } from "utilities/hooks"; // @ts-ignore
import { useDeepEffect } from "utilities/hooks";
import Button from "components/buttons/Button";
@ -74,6 +74,7 @@ const EditPackForm = ({
isPremiumTier,
formData,
}: IEditPackForm): JSX.Element => {
const [errors, setErrors] = useState<{ [key: string]: any }>({});
const [packName, setPackName] = useState<string>(formData.name);
const [packDescription, setPackDescription] = useState<string>(
formData.description
@ -101,6 +102,13 @@ const EditPackForm = ({
};
const onFormSubmit = () => {
if (packName === "") {
return setErrors({
...errors,
name: "Pack name must be present",
});
}
handleSubmit({
name: packName,
description: packDescription,
@ -117,6 +125,7 @@ const EditPackForm = ({
placeholder="Name"
label="Name"
name="name"
error={errors.name}
inputWrapperClass={`${baseClass}__pack-title`}
/>
<InputField

View file

@ -1,84 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import Button from "components/buttons/Button";
import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
import InputField from "components/forms/fields/InputField";
import SelectTargetsDropdown from "components/forms/fields/SelectTargetsDropdown";
import validate from "./validate";
const fieldNames = ["name", "description", "targets"];
const baseClass = "pack-form";
class PackForm extends Component {
static propTypes = {
baseError: PropTypes.string,
className: PropTypes.string,
fields: PropTypes.shape({
description: formFieldInterface.isRequired,
targets: formFieldInterface.isRequired,
name: formFieldInterface.isRequired,
}).isRequired,
handleSubmit: PropTypes.func,
onFetchTargets: PropTypes.func,
selectedTargetsCount: PropTypes.number,
isPremiumTier: PropTypes.bool,
};
render() {
const {
baseError,
className,
fields,
handleSubmit,
onFetchTargets,
selectedTargetsCount,
isPremiumTier,
} = this.props;
const packFormClass = classnames(baseClass, className);
return (
<form className={packFormClass} onSubmit={handleSubmit}>
<h1>New pack</h1>
{baseError && <div className="form__base-error">{baseError}</div>}
<InputField
{...fields.name}
placeholder="Query pack title"
label="Name"
inputWrapperClass={`${baseClass}__pack-title`}
/>
<InputField
{...fields.description}
inputWrapperClass={`${baseClass}__pack-description`}
label="Description"
placeholder="Add a description of your pack"
type="textarea"
/>
<div className={`${baseClass}__pack-targets`}>
<SelectTargetsDropdown
{...fields.targets}
label="Select pack targets"
onSelect={fields.targets.onChange}
onFetchTargets={onFetchTargets}
selectedTargets={fields.targets.value}
targetsCount={selectedTargetsCount}
isPremiumTier={isPremiumTier}
/>
</div>
<div className={`${baseClass}__pack-buttons`}>
<Button type="submit" variant="brand">
Save query pack
</Button>
</div>
</form>
);
}
}
export default Form(PackForm, {
fields: fieldNames,
validate,
});

View file

@ -1,59 +0,0 @@
import React from "react";
import { mount } from "enzyme";
import { noop } from "lodash";
import { fillInFormInput } from "test/helpers";
import targetMock from "test/target_mock";
import PackForm from "./index";
describe("PackForm - component", () => {
beforeEach(targetMock);
it("renders the base error", () => {
const baseError = "Pack already exists";
const formWithError = mount(
<PackForm serverErrors={{ base: baseError }} handleSubmit={noop} />
);
const formWithoutError = mount(<PackForm handleSubmit={noop} />);
expect(formWithError.text()).toContain(baseError);
expect(formWithoutError.text()).not.toContain(baseError);
});
it("renders the correct components", () => {
const form = mount(<PackForm />);
expect(form.find("InputField").length).toEqual(2);
expect(form.find("SelectTargetsDropdown").length).toEqual(1);
expect(form.find("Button").length).toEqual(1);
});
it("validates the query pack name field", () => {
const handleSubmitSpy = jest.fn();
const form = mount(<PackForm handleSubmit={handleSubmitSpy} />);
form.find("form").simulate("submit");
expect(handleSubmitSpy).not.toHaveBeenCalled();
const formFieldProps = form.find("PackForm").prop("fields");
expect(formFieldProps.name).toMatchObject({
error: "Title field must be completed",
});
});
it("calls the handleSubmit prop when a valid form is submitted", () => {
const handleSubmitSpy = jest.fn();
const form = mount(<PackForm handleSubmit={handleSubmitSpy} />).find(
"form"
);
const nameField = form.find("InputField").find({ name: "name" });
fillInFormInput(nameField, "Mac OS Attacks");
form.simulate("submit");
expect(handleSubmitSpy).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,123 @@
import React, { Component, useState } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import Button from "components/buttons/Button";
import { IQuery } from "interfaces/query";
import { ITarget, ITargetsAPIResponse } from "interfaces/target";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
// @ts-ignore
import SelectTargetsDropdown from "components/forms/fields/SelectTargetsDropdown";
const baseClass = "pack-form";
interface IPackForm {
className?: string;
handleSubmit: (formData: IEditPackFormData) => void;
onFetchTargets?: (
query: IQuery,
targetsResponse: ITargetsAPIResponse
) => boolean;
selectedTargetsCount?: number;
isPremiumTier?: boolean;
formData: IEditPackFormData;
baseError: any;
}
interface IEditPackFormData {
name: string;
description: string;
targets: ITarget[];
}
interface IFormErrors {
name?: string;
}
const EditPackForm = ({
className,
handleSubmit,
onFetchTargets,
selectedTargetsCount,
isPremiumTier,
formData,
baseError,
}: IPackForm): JSX.Element => {
const [errors, setErrors] = useState<{ [key: string]: any }>({});
const [packName, setPackName] = useState<string>("");
const [packDescription, setPackDescription] = useState<string>("");
const [packFormTargets, setPackFormTargets] = useState<ITarget[] | []>([]);
const onChangePackName = (value: string) => {
setPackName(value);
};
const onChangePackDescription = (value: string) => {
setPackDescription(value);
};
const onChangePackTargets = (value: ITarget[]) => {
setPackFormTargets(value);
};
const onFormSubmit = () => {
if (packName === "") {
return setErrors({
...errors,
name: "Pack name must be present",
});
}
handleSubmit({
name: packName,
description: packDescription,
targets: [...packFormTargets],
});
};
const packFormClass = classnames(baseClass, className);
return (
<form className={packFormClass} onSubmit={onFormSubmit}>
<h1>New pack</h1>
{baseError && <div className="form__base-error">{baseError}</div>}
<InputField
onChange={onChangePackName}
value={packName}
placeholder="Name"
label="Name"
name="name"
error={errors.name}
inputWrapperClass={`${baseClass}__pack-title`}
/>
<InputField
onChange={onChangePackDescription}
value={packDescription}
inputWrapperClass={`${baseClass}__pack-description`}
label="Description"
name="description"
placeholder="Add a description of your pack"
type="textarea"
/>
<div className={`${baseClass}__pack-targets`}>
<SelectTargetsDropdown
label="Select pack targets"
name="selected-pack-targets"
onFetchTargets={onFetchTargets}
onSelect={onChangePackTargets}
selectedTargets={packFormTargets}
targetsCount={selectedTargetsCount}
isPremiumTier={isPremiumTier}
/>
</div>
<div className={`${baseClass}__pack-buttons`}>
<Button onClick={onFormSubmit} variant="brand">
Save query pack
</Button>
</div>
</form>
);
};
export default EditPackForm;

View file

@ -1,15 +0,0 @@
import { size } from "lodash";
const validate = (formData) => {
const errors = {};
if (!formData.name) {
errors.name = "Title field must be completed";
}
const valid = !size(errors);
return { valid, errors };
};
export default validate;

View file

@ -229,10 +229,22 @@ const EditPacksPage = ({
window.scrollTo(0, 0);
dispatch(renderFlash("success", `Successfully updated this pack.`));
})
.catch(() => {
dispatch(
renderFlash("error", `Could not update pack. Please try again.`)
);
.catch((response) => {
if (
response.errors[0].reason.slice(0, 27) ===
"Error 1062: Duplicate entry"
) {
dispatch(
renderFlash(
"error",
"Unable to update pack. Pack names must be unique."
)
);
} else {
dispatch(
renderFlash("error", `Could not update pack. Please try again.`)
);
}
});
};

View file

@ -44,7 +44,7 @@ export class PackComposerPage extends Component {
visitPackPage = (packID) => {
const { dispatch } = this.props;
dispatch(push(PATHS.PACK({ id: packID })));
dispatch(push(PATHS.PACK(packID)));
return false;
};
@ -58,13 +58,20 @@ export class PackComposerPage extends Component {
.then((pack) => {
const { id: packID } = pack;
return visitPackPage(packID);
})
.then(() => {
dispatch(renderFlash("success", `Pack successfully created.`));
visitPackPage(packID);
})
.catch(() => {
dispatch(renderFlash("error", "Unable to create pack."));
.catch((response) => {
if (response.base.slice(0, 27) === "Error 1062: Duplicate entry") {
dispatch(
renderFlash(
"error",
"Unable to create pack. Pack names must be unique."
)
);
} else {
dispatch(renderFlash("error", "Unable to create pack."));
}
});
};

View file

@ -19,14 +19,6 @@ describe("PackComposerPage - component", () => {
expect(page.length).toEqual(1);
});
it("renders a PackForm component", () => {
const page = mount(
connectedComponent(ConnectedPacksComposerPage, { mockStore })
);
expect(page.find("PackForm").length).toEqual(1);
});
it("renders a PackInfoSidePanel component", () => {
const page = mount(
connectedComponent(ConnectedPacksComposerPage, { mockStore })