mirror of
https://github.com/fleetdm/fleet
synced 2026-05-08 09:40:49 +00:00
## For #28049 , #28610 - **Implement front end ability to enable or disable conditional access on a per-policy basis** - **Update policy status UI to include new "action required" state, representing a failed policy on a host with conditional access enabled** - Additional improvements <img width="1624" alt="Screenshot 2025-04-29 at 1 32 33 PM" src="https://github.com/user-attachments/assets/960b3348-b0e2-48b8-bcff-28f91f64fd01" /> <img width="1624" alt="Screenshot 2025-04-29 at 12 15 39 PM" src="https://github.com/user-attachments/assets/b0e0cf1f-a693-4e0b-b18a-a44ee258975f" /> <img width="1624" alt="Screenshot 2025-04-29 at 12 15 49 PM" src="https://github.com/user-attachments/assets/15f7bea1-7338-4997-93bf-8baeb308e3f0" /> <img width="1400" alt="updated policies table headers" src="https://github.com/user-attachments/assets/164fd84a-a9ee-4dfe-8d73-b4e82e27edbc" /> - [x] Changes file added for user-visible changes in `changes/` - [ ] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
186 lines
5 KiB
TypeScript
186 lines
5 KiB
TypeScript
import React, { ReactNode, KeyboardEvent, useEffect, useRef } from "react";
|
|
import classnames from "classnames";
|
|
import { noop, pick } from "lodash";
|
|
|
|
import FormField from "components/forms/FormField";
|
|
import { IFormFieldProps } from "components/forms/FormField/FormField";
|
|
import TooltipWrapper from "components/TooltipWrapper";
|
|
import Icon from "components/Icon";
|
|
|
|
const baseClass = "fleet-checkbox";
|
|
|
|
export interface ICheckboxProps {
|
|
children?: ReactNode;
|
|
className?: string;
|
|
/** readOnly displays a non-editable field */
|
|
readOnly?: boolean;
|
|
/** disabled displays a greyed out non-editable field */
|
|
disabled?: boolean;
|
|
name?: string;
|
|
onChange?: any; // TODO: meant to be an event; figure out type for this
|
|
onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
|
|
value?: boolean | null;
|
|
wrapperClassName?: string;
|
|
indeterminate?: boolean;
|
|
parseTarget?: boolean;
|
|
/** to display over the checkbox label */
|
|
labelTooltipContent?: React.ReactNode;
|
|
/** to display over the checkbox icon */
|
|
iconTooltipContent?: React.ReactNode;
|
|
isLeftLabel?: boolean;
|
|
helpText?: React.ReactNode;
|
|
/** Use in table action only
|
|
* Do not use on forms as enter key reserved for submit */
|
|
enableEnterToCheck?: boolean;
|
|
}
|
|
|
|
const Checkbox = (props: ICheckboxProps) => {
|
|
const {
|
|
children,
|
|
className,
|
|
readOnly = false,
|
|
disabled = false,
|
|
name,
|
|
onChange = noop,
|
|
onBlur = noop,
|
|
value = false,
|
|
wrapperClassName,
|
|
indeterminate = false,
|
|
parseTarget,
|
|
labelTooltipContent,
|
|
iconTooltipContent,
|
|
isLeftLabel,
|
|
helpText,
|
|
enableEnterToCheck = false,
|
|
} = props;
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (inputRef.current) {
|
|
inputRef.current.indeterminate = indeterminate;
|
|
}
|
|
}, [indeterminate]);
|
|
|
|
const handleChange = (
|
|
event: React.MouseEvent | React.KeyboardEvent
|
|
): void => {
|
|
event.preventDefault();
|
|
if (readOnly || disabled) return;
|
|
|
|
// If indeterminate, set to true; otherwise, toggle the current value
|
|
const newValue = indeterminate || !value;
|
|
|
|
if (parseTarget) {
|
|
onChange({ name, value: newValue });
|
|
} else {
|
|
onChange(newValue);
|
|
}
|
|
|
|
// Update the hidden input
|
|
if (inputRef.current) {
|
|
inputRef.current.checked = newValue;
|
|
}
|
|
};
|
|
|
|
/** Manual implementation of spacebar toggling checkboxes (default behavior)
|
|
* since we're using a custom div instead of a native checkbox
|
|
* Enter key intended to toggle table checkboxes only */
|
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
|
|
if (event.key === " " || (enableEnterToCheck && event.key === "Enter")) {
|
|
handleChange(event);
|
|
}
|
|
};
|
|
|
|
const checkBoxClass = classnames(
|
|
{ inverse: isLeftLabel },
|
|
className,
|
|
baseClass
|
|
);
|
|
|
|
const checkBoxLabelClass = classnames(checkBoxClass, {
|
|
[`${baseClass}__label--read-only`]: readOnly || disabled,
|
|
[`${baseClass}__label--disabled`]: disabled,
|
|
});
|
|
|
|
const formFieldProps = {
|
|
...pick(props, ["helpText", "label", "error", "name"]),
|
|
className: wrapperClassName,
|
|
type: "checkbox",
|
|
} as IFormFieldProps;
|
|
|
|
const getIconName = () => {
|
|
if (indeterminate) return "checkbox-indeterminate";
|
|
if (value) return "checkbox";
|
|
return "checkbox-unchecked";
|
|
};
|
|
|
|
const renderIcon = () => {
|
|
const icon = (
|
|
<Icon
|
|
name={getIconName()}
|
|
className={`${baseClass}__icon ${baseClass}__icon--${getIconName()}`}
|
|
/>
|
|
);
|
|
if (iconTooltipContent) {
|
|
return (
|
|
<TooltipWrapper
|
|
tipContent={iconTooltipContent}
|
|
clickable={false}
|
|
underline={false}
|
|
showArrow
|
|
position="right"
|
|
tipOffset={8}
|
|
>
|
|
{icon}
|
|
</TooltipWrapper>
|
|
);
|
|
}
|
|
return icon;
|
|
};
|
|
|
|
return (
|
|
<FormField {...formFieldProps}>
|
|
<label htmlFor={name}>
|
|
<input
|
|
type="checkbox"
|
|
ref={inputRef}
|
|
name={name}
|
|
checked={!!value}
|
|
onChange={noop} // Empty onChange to avoid React warning
|
|
disabled={disabled || readOnly}
|
|
style={{ display: "none" }} // Hide the input
|
|
id={name}
|
|
/>
|
|
<div
|
|
role="checkbox"
|
|
aria-label={name}
|
|
aria-checked={indeterminate ? "mixed" : value || undefined}
|
|
aria-readonly={readOnly}
|
|
aria-disabled={disabled}
|
|
tabIndex={disabled ? -1 : 0}
|
|
className={checkBoxLabelClass}
|
|
onClick={handleChange}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={onBlur}
|
|
>
|
|
{renderIcon()}
|
|
{labelTooltipContent ? (
|
|
<span className={`${baseClass}__label-tooltip tooltip`}>
|
|
<TooltipWrapper
|
|
tipContent={labelTooltipContent}
|
|
clickable={false} // Not block form behind tooltip
|
|
>
|
|
{children}
|
|
</TooltipWrapper>
|
|
</span>
|
|
) : (
|
|
<span className={`${baseClass}__label`}>{children}</span>
|
|
)}
|
|
</div>
|
|
</label>
|
|
</FormField>
|
|
);
|
|
};
|
|
|
|
export default Checkbox;
|