mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Packs UI: Create pack navigates to pack's edit page (#2581)
This commit is contained in:
parent
3369436741
commit
fdfabf9e32
10 changed files with 163 additions and 177 deletions
1
changes/issue-2569-fix-create-pack-page-bug
Normal file
1
changes/issue-2569-fix-create-pack-page-bug
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Redirected to edit pack page after creating a pack
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
123
frontend/components/forms/packs/PackForm/PackForm.tsx
Normal file
123
frontend/components/forms/packs/PackForm/PackForm.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.`)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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."));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
Loading…
Reference in a new issue