mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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:
parent
11daebac39
commit
3af64748ab
17 changed files with 298 additions and 278 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
67
frontend/components/forms/FormField/FormField.tsx
Normal file
67
frontend/components/forms/FormField/FormField.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
71
frontend/components/forms/fields/Checkbox/Checkbox.tsx
Normal file
71
frontend/components/forms/fields/Checkbox/Checkbox.tsx
Normal 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;
|
||||
23
frontend/interfaces/registration_form_data.ts
Normal file
23
frontend/interfaces/registration_form_data.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -140,7 +140,6 @@ const SelectedTeamsForm = (props: ISelectedTeamsFormProps): JSX.Element => {
|
|||
onChange={(newValue: boolean) =>
|
||||
updateSelectedTeams(teamItem.id, newValue, "checkbox")
|
||||
}
|
||||
testId={`${name}-checkbox`}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"target": "ES2016",
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"./frontend/**/*"
|
||||
|
|
|
|||
15
yarn.lock
15
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue