Pressing Enter on setup's Confirmation page (#1141)

* #917 fixed enter key for last page; TS overhaul

* #917 clean up

* Update frontend/components/forms/FormField/FormField.tsx

Co-authored-by: Zach Wasserman <zach@fleetdm.com>

* #917 fixed tests and linted

Co-authored-by: Zach Wasserman <zach@fleetdm.com>
This commit is contained in:
Martavis Parker 2021-06-18 13:33:45 -07:00 committed by GitHub
parent 11daebac39
commit 3af64748ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 298 additions and 278 deletions

View file

@ -9,7 +9,7 @@ interface IButtonProps {
children: React.ReactChild;
className?: string;
disabled?: boolean;
onClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: (evt: React.MouseEvent<HTMLButtonElement>) => void;
size?: string;
tabIndex?: number;
type?: "button" | "submit" | "reset";

View file

@ -1,75 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
const baseClass = "form-field";
class FormField extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
error: PropTypes.string,
hint: PropTypes.oneOfType([
PropTypes.array,
PropTypes.node,
PropTypes.string,
]),
label: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string,
PropTypes.node,
]),
name: PropTypes.string,
type: PropTypes.string,
};
renderLabel = () => {
const { error, label, name } = this.props;
const labelWrapperClasses = classnames(`${baseClass}__label`, {
[`${baseClass}__label--error`]: error,
});
if (!label) {
return false;
}
return (
<label className={labelWrapperClasses} htmlFor={name}>
{error || label}
</label>
);
};
renderHint = () => {
const { hint } = this.props;
if (hint) {
return <span className={`${baseClass}__hint`}>{hint}</span>;
}
return false;
};
render() {
const { renderLabel, renderHint } = this;
const { children, className, type } = this.props;
const formFieldClass = classnames(
baseClass,
{
[`${baseClass}--${type}`]: type,
},
className
);
return (
<div className={formFieldClass}>
{renderLabel()}
{children}
{renderHint()}
</div>
);
}
}
export default FormField;

View file

@ -0,0 +1,67 @@
import React from "react";
import classnames from "classnames";
import { isEmpty } from "lodash";
const baseClass = "form-field";
export interface IFormFieldProps {
children: JSX.Element;
className: string;
error: string;
hint: Array<any> | JSX.Element | string;
label: Array<any> | JSX.Element | string;
name: string;
type: string;
}
const FormField = ({
children,
className,
error,
hint,
label,
name,
type,
}: IFormFieldProps) => {
const renderLabel = () => {
const labelWrapperClasses = classnames(`${baseClass}__label`, {
[`${baseClass}__label--error`]: !isEmpty(error),
});
if (!label) {
return false;
}
return (
<label className={labelWrapperClasses} htmlFor={name}>
{error || label}
</label>
);
};
const renderHint = () => {
if (hint) {
return <span className={`${baseClass}__hint`}>{hint}</span>;
}
return false;
};
const formFieldClass = classnames(
baseClass,
{
[`${baseClass}--${type}`]: !isEmpty(type),
},
className
);
return (
<div className={formFieldClass}>
{renderLabel()}
{children}
{renderHint()}
</div>
);
};
export default FormField;

View file

@ -1,120 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import Button from "components/buttons/Button";
import formDataInterface from "interfaces/registration_form_data";
import Checkbox from "components/forms/fields/Checkbox";
const baseClass = "confirm-user-reg";
class ConfirmationPage extends Component {
static propTypes = {
className: PropTypes.string,
currentPage: PropTypes.bool,
formData: formDataInterface,
handleSubmit: PropTypes.func,
};
componentDidUpdate(prevProps) {
if (
this.props.currentPage &&
this.props.currentPage !== prevProps.currentPage
) {
// Component has a transition duration of 300ms set in
// RegistrationForm/_styles.scss. We need to wait 300ms before
// calling .focus() to preserve smooth transition.
setTimeout(() => {
this.firstInput && this.firstInput.input.focus();
}, 300);
}
}
importOsqueryConfig = () => {
const disableImport = true;
if (disableImport) {
return false;
}
return (
<div className={`${baseClass}__import`}>
<Checkbox name="import-install">
<p>
I am migrating an existing <strong>osquery</strong> installation.
</p>
<p>
Take me to the <strong>Import Configuration</strong> page.
</p>
</Checkbox>
</div>
);
};
render() {
const { importOsqueryConfig } = this;
const {
className,
currentPage,
handleSubmit,
formData: {
email,
server_url: fleetWebAddress,
org_name: orgName,
username,
},
} = this.props;
const tabIndex = currentPage ? 1 : -1;
const confirmRegClasses = classnames(className, baseClass);
return (
<form onSubmit={handleSubmit} className={confirmRegClasses}>
<div className={`${baseClass}__wrapper`}>
<table className={`${baseClass}__table`}>
<caption>Administrator configuration</caption>
<tbody>
<tr>
<th>Username:</th>
<td>{username}</td>
</tr>
<tr>
<th>Email:</th>
<td>{email}</td>
</tr>
<tr>
<th>Organization:</th>
<td>{orgName}</td>
</tr>
<tr>
<th>Fleet URL:</th>
<td>
<span
className={`${baseClass}__table-url`}
title={fleetWebAddress}
>
{fleetWebAddress}
</span>
</td>
</tr>
</tbody>
</table>
{importOsqueryConfig()}
</div>
<Button
type="submit"
tabIndex={tabIndex}
disabled={!currentPage}
className="button button--brand"
autofocus
>
Finish
</Button>
</form>
);
}
}
export default ConfirmationPage;

View file

@ -9,7 +9,7 @@ describe("ConfirmationPage - form", () => {
username: "jmeller",
email: "jason@Fleet.co",
org_name: "Kolide",
server_url: "http://Fleet.Fleet.co",
fleet_web_address: "http://Fleet.Fleet.co",
};
it("renders the user information", () => {
@ -20,7 +20,7 @@ describe("ConfirmationPage - form", () => {
expect(form.text()).toContain(formData.username);
expect(form.text()).toContain(formData.email);
expect(form.text()).toContain(formData.org_name);
expect(form.text()).toContain(formData.server_url);
expect(form.text()).toContain(formData.fleet_web_address);
});
it("submits the form", () => {

View file

@ -0,0 +1,117 @@
import React, { useEffect } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
import { IRegistrationFormData } from "interfaces/registration_form_data";
import Checkbox from "components/forms/fields/Checkbox";
const baseClass = "confirm-user-reg";
interface IConfirmationPageProps {
className: string;
currentPage: boolean;
formData: IRegistrationFormData;
handleSubmit: any; // TODO: meant to be an event; figure out type for this
}
const ConfirmationPage = ({
className,
currentPage,
formData,
handleSubmit,
}: IConfirmationPageProps) => {
useEffect(() => {
if (currentPage) {
// Component has a transition duration of 300ms set in
// RegistrationForm/_styles.scss. We need to wait 300ms before
// calling .focus() to preserve smooth transition.
setTimeout(() => {
// wanted to use React ref here instead of class but ref is already used
// in Button.tsx, which could break other button uses
const confirmationButton = document.querySelector(
`.${baseClass} button.button--brand`
) as HTMLElement;
confirmationButton?.focus();
}, 300);
}
}, [currentPage]);
const importOsqueryConfig = () => {
const disableImport = true;
if (disableImport) {
return false;
}
return (
<div className={`${baseClass}__import`}>
<Checkbox name="import-install">
<p>
I am migrating an existing <strong>osquery</strong> installation.
</p>
<p>
Take me to the <strong>Import Configuration</strong> page.
</p>
</Checkbox>
</div>
);
};
const {
email,
fleet_web_address: fleetWebAddress,
org_name: orgName,
username,
} = formData;
const tabIndex = currentPage ? 1 : -1;
const confirmRegClasses = classnames(className, baseClass);
return (
<form onSubmit={handleSubmit} className={confirmRegClasses}>
<div className={`${baseClass}__wrapper`}>
<table className={`${baseClass}__table`}>
<caption>Administrator configuration</caption>
<tbody>
<tr>
<th>Username:</th>
<td>{username}</td>
</tr>
<tr>
<th>Email:</th>
<td>{email}</td>
</tr>
<tr>
<th>Organization:</th>
<td>{orgName}</td>
</tr>
<tr>
<th>Fleet URL:</th>
<td>
<span
className={`${baseClass}__table-url`}
title={fleetWebAddress}
>
{fleetWebAddress}
</span>
</td>
</tr>
</tbody>
</table>
{importOsqueryConfig()}
</div>
<Button
type="submit"
tabIndex={tabIndex}
disabled={!currentPage}
className="button button--brand"
>
Finish
</Button>
</form>
);
};
export default ConfirmationPage;

View file

@ -1,79 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import { noop, pick } from "lodash";
import FormField from "components/forms/FormField";
const baseClass = "kolide-checkbox";
class Checkbox extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
name: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.bool,
wrapperClassName: PropTypes.string,
indeterminate: PropTypes.bool,
};
static defaultProps = {
disabled: false,
onChange: noop,
};
handleChange = () => {
const { onChange, value } = this.props;
return onChange(!value);
};
render() {
const { handleChange } = this;
const {
children,
className,
disabled,
name,
value,
wrapperClassName,
indeterminate,
} = this.props;
const checkBoxClass = classnames(baseClass, className);
const formFieldProps = pick(this.props, ["hint", "label", "error", "name"]);
const checkBoxTickClass = classnames(`${checkBoxClass}__tick`, {
[`${checkBoxClass}__tick--disabled`]: disabled,
[`${checkBoxClass}__tick--indeterminate`]: indeterminate,
});
return (
<FormField
{...formFieldProps}
className={wrapperClassName}
type="checkbox"
>
<label htmlFor={name} className={checkBoxClass}>
<input
checked={value}
className={`${checkBoxClass}__input`}
disabled={disabled}
id={name}
name={name}
onChange={handleChange}
type="checkbox"
ref={(element) => {
element && (element.indeterminate = indeterminate);
}}
/>
<span className={checkBoxTickClass} />
<span className={`${checkBoxClass}__label`}>{children}</span>
</label>
</FormField>
);
}
}
export default Checkbox;

View file

@ -0,0 +1,71 @@
import React from "react";
import classnames from "classnames";
import { noop, pick } from "lodash";
import FormField from "components/forms/FormField";
import { IFormFieldProps } from "components/forms/FormField/FormField";
const baseClass = "kolide-checkbox";
interface ICheckboxProps {
children?: JSX.Element | Array<JSX.Element> | string;
className?: string;
disabled?: boolean;
name?: string;
onChange?: any; // TODO: meant to be an event; figure out type for this
value?: boolean;
wrapperClassName?: string;
indeterminate?: boolean;
}
const Checkbox = (props: ICheckboxProps) => {
const {
children,
className,
disabled = false,
name,
onChange = noop,
value,
wrapperClassName,
indeterminate,
} = props;
const handleChange = () => {
return onChange(!value);
};
const checkBoxClass = classnames(baseClass, className);
const formFieldProps = {
...pick(props, ["hint", "label", "error", "name"]),
className: wrapperClassName,
type: "checkbox",
} as IFormFieldProps;
const checkBoxTickClass = classnames(`${checkBoxClass}__tick`, {
[`${checkBoxClass}__tick--disabled`]: disabled,
[`${checkBoxClass}__tick--indeterminate`]: indeterminate,
});
return (
<FormField {...formFieldProps}>
<label htmlFor={name} className={checkBoxClass}>
<input
checked={value}
className={`${checkBoxClass}__input`}
disabled={disabled}
id={name}
name={name}
onChange={handleChange}
type="checkbox"
ref={(element) => {
element && indeterminate && (element.indeterminate = indeterminate);
}}
/>
<span className={checkBoxTickClass} />
<span className={`${checkBoxClass}__label`}>{children}</span>
</label>
</FormField>
);
};
export default Checkbox;

View file

@ -0,0 +1,23 @@
import PropTypes from "prop-types";
export default PropTypes.shape({
username: PropTypes.string,
password: PropTypes.string,
password_confirmation: PropTypes.string,
email: PropTypes.string,
org_name: PropTypes.string,
org_web_url: PropTypes.string,
org_logo_url: PropTypes.string,
fleet_web_address: PropTypes.string,
});
export interface IRegistrationFormData {
username: string;
password: string;
password_confirmation: string;
email: string;
org_name: string;
org_web_url: string;
org_logo_url: string;
fleet_web_address: string;
}

View file

@ -140,7 +140,6 @@ const SelectedTeamsForm = (props: ISelectedTeamsFormProps): JSX.Element => {
onChange={(newValue: boolean) =>
updateSelectedTeams(teamItem.id, newValue, "checkbox")
}
testId={`${name}-checkbox`}
>
{name}
</Checkbox>

View file

@ -143,6 +143,7 @@
"@tsconfig/recommended": "^1.0.1",
"@types/classnames": "0.0.32",
"@types/cypress": "^1.1.3",
"@types/enzyme": "^3.10.8",
"@types/expect": "1.20.3",
"@types/js-md5": "^0.4.2",
"@types/js-yaml": "^4.0.1",

View file

@ -5,6 +5,7 @@
"target": "ES2016",
"sourceMap": true,
"jsx": "react",
"allowSyntheticDefaultImports": true
},
"include": [
"./frontend/**/*"

View file

@ -1544,6 +1544,13 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/cheerio@*":
version "0.22.29"
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.29.tgz#7115e9688bfc9e2f2730327c674b3d6a7e753e09"
integrity sha512-rNX1PsrDPxiNiyLnRKiW2NXHJFHqx0Fl3J2WsZq0MTBspa/FgwlqhXJE2crIcc+/2IglLHtSWw7g053oUR8fOg==
dependencies:
"@types/node" "*"
"@types/classnames@0.0.32":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-0.0.32.tgz#449abcd9a826807811ef101e58df9f83cfc61713"
@ -1556,6 +1563,14 @@
dependencies:
cypress "*"
"@types/enzyme@^3.10.8":
version "3.10.8"
resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.8.tgz#ad7ac9d3af3de6fd0673773123fafbc63db50d42"
integrity sha512-vlOuzqsTHxog6PV79+tvOHFb6hq4QZKMq1lLD9MaWD1oec2lHTKndn76XOpSwCA0oFTaIbKVPrgM3k78Jjd16g==
dependencies:
"@types/cheerio" "*"
"@types/react" "*"
"@types/expect@1.20.3":
version "1.20.3"
resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.3.tgz#a99d059ace4bd051332faf03f5b4b4266149a501"