add verified status to UI for profile statuses (#11886)

relates to #11238

This implements the Verified status for the profile statute on the macOS
settings pages and the Host Details and My Device pages.

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Gabriel Hernandez 2023-06-06 15:52:10 +01:00 committed by GitHub
parent f140797938
commit 2c9c9b4f0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 208 additions and 94 deletions

View File

@ -0,0 +1 @@
- add "verified" profile status to fleet UI

View File

@ -1,4 +1,19 @@
import { IHost } from "interfaces/host";
import { IHostMacMdmProfile } from "interfaces/mdm";
const DEFAULT_HOST_PROFILE_MOCK: IHostMacMdmProfile = {
profile_id: 1,
name: "Test Profile",
operation_type: "install",
status: "verified",
detail: "This is verified",
};
export const createMockHostMacMdmProfile = (
overrides?: Partial<IHostMacMdmProfile>
): IHostMacMdmProfile => {
return { ...DEFAULT_HOST_PROFILE_MOCK, ...overrides };
};
const DEFAULT_HOST_MOCK: IHost = {
id: 1,

View File

@ -1,5 +1,5 @@
import { IHostMdmData } from "interfaces/host";
import { IMdmSolution } from "interfaces/mdm";
import { IMdmSolution, IMdmProfile } from "interfaces/mdm";
const DEFAULT_MDM_SOLUTION_MOCK: IMdmSolution = {
id: 1,
@ -14,6 +14,21 @@ export const createMockMdmSolution = (
return { ...DEFAULT_MDM_SOLUTION_MOCK, ...overrides };
};
const DEFAULT_MDM_PROFILE_DATA: IMdmProfile = {
profile_id: 1,
team_id: 0,
name: "Test Profile",
identifier: "com.test.profile",
created_at: "2021-01-01T00:00:00Z",
updated_at: "2021-01-01T00:00:00Z",
};
export const createMockMdmProfile = (
overrides?: Partial<IMdmProfile>
): IMdmProfile => {
return { ...DEFAULT_MDM_PROFILE_DATA, ...overrides };
};
const DEFAULT_HOST_MDM_DATA: IHostMdmData = {
encryption_key_available: false,
enrollment_status: "On (automatic)",

View File

@ -70,7 +70,7 @@ export interface IMdmProfilesResponse {
profiles: IMdmProfile[] | null;
}
export type MdmProfileStatus = "verifying" | "pending" | "failed";
export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
export type MacMdmProfileOperationType = "remove" | "install";
@ -83,22 +83,13 @@ export interface IHostMacMdmProfile {
detail: string;
}
export interface IFileVaultSummaryResponse {
verifying: number;
action_required: number;
enforcing: number;
failed: number;
removing_enforcement: number;
}
export enum FileVaultProfileStatus {
VERIFIED = "verified",
VERIFYING = "verifying",
ACTION_REQUIRED = "action_required",
ENFORCING = "enforcing",
FAILED = "failed",
REMOVING_ENFORCEMENT = "removing_enforcement",
}
export type FileVaultProfileStatus =
| "verified"
| "verifying"
| "action_required"
| "enforcing"
| "failed"
| "removing_enforcement";
// // TODO: update when list profiles API returns identifier
// export const FLEET_FILEVAULT_PROFILE_IDENTIFIER =

View File

@ -18,26 +18,34 @@ interface IAggregateDisplayOption {
}
const AGGREGATE_STATUS_DISPLAY_OPTIONS: IAggregateDisplayOption[] = [
{
value: "verified",
text: "Verified",
iconName: "success",
tooltipText:
"These hosts installed all configuration profiles. Fleet verified with osquery.",
},
{
value: "verifying",
text: "Verifying",
iconName: "success-partial",
tooltipText:
"Hosts that told Fleet all settings are enforced. Fleet is verifying.",
"These hosts acknowledged all MDM commands to install configuration profiles. " +
"Fleet is verifying the profiles are installed with osquery.",
},
{
value: "pending",
text: "Pending",
iconName: "pending-partial",
tooltipText:
"Hosts that will have settings enforced when the hosts come online.",
"These hosts will receive MDM commands to install configuration profiles when the hosts come online.",
},
{
value: "failed",
text: "Failed",
iconName: "error",
tooltipText:
"Hosts that failed to apply settings. Click on a host to view error(s).",
"These hosts failed to install configuration profiles. Click on a host to view error(s).",
},
];

View File

@ -1,8 +1,7 @@
import React from "react";
import { useQuery } from "react-query";
import { IFileVaultSummaryResponse } from "interfaces/mdm";
import mdmAPI from "services/entities/mdm";
import mdmAPI, { IFileVaultSummaryResponse } from "services/entities/mdm";
import TableContainer from "components/TableContainer";
import EmptyTable from "components/EmptyTable";
@ -23,11 +22,10 @@ const DEFAULT_SORT_HEADER = "hosts";
const DEFAULT_SORT_DIRECTION = "asc";
const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
const { data, error } = useQuery<
IFileVaultSummaryResponse,
Error,
IFileVaultSummaryResponse
>(
const {
data: diskEncryptionStatusData,
error: diskEncryptionStatusError,
} = useQuery<IFileVaultSummaryResponse, Error, IFileVaultSummaryResponse>(
["disk-encryption-summary", currentTeamId],
() => mdmAPI.getDiskEncryptionAggregate(currentTeamId),
{
@ -37,13 +35,14 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
);
const tableHeaders = generateTableHeaders();
const tableData = generateTableData(data, currentTeamId);
if (error) {
const tableData = generateTableData(diskEncryptionStatusData, currentTeamId);
if (diskEncryptionStatusError) {
return <DataError />;
}
if (!data) return null;
if (!diskEncryptionStatusData) return null;
return (
<div className={baseClass}>

View File

@ -1,9 +1,7 @@
import React from "react";
import {
FileVaultProfileStatus,
IFileVaultSummaryResponse,
} from "interfaces/mdm";
import { FileVaultProfileStatus } from "interfaces/mdm";
import { IFileVaultSummaryResponse } from "services/entities/mdm";
import TextCell from "components/TableContainer/DataTable/TextCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
@ -115,19 +113,22 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
verified: {
displayName: "Verified",
statusName: "success",
value: FileVaultProfileStatus.VERIFIED,
tooltip: "Disk encryption on and key stored in Fleet. Fleet has verified.",
value: "verified",
tooltip:
"These hosts turned disk encryption on and sent their key to Fleet. Fleet verified with osquery.",
},
verifying: {
displayName: "Verifying",
statusName: "successPartial",
value: FileVaultProfileStatus.VERIFYING,
tooltip: "Disk encryption on and key stored in Fleet. Fleet will verify.",
value: "verifying",
tooltip:
"These hosts acknowledged the MDM command to install disk encryption profile. " +
"Fleet is verifying with osquery and retrieving the disk encryption key. This may take up to one hour.",
},
action_required: {
displayName: "Action required (pending)",
statusName: "pendingPartial",
value: FileVaultProfileStatus.ACTION_REQUIRED,
value: "action_required",
tooltip: (
<>
Ask the end user to follow <b>Disk encryption</b> instructions on their{" "}
@ -138,19 +139,21 @@ const STATUS_CELL_VALUES: Record<FileVaultProfileStatus, IStatusCellValue> = {
enforcing: {
displayName: "Enforcing (pending)",
statusName: "pendingPartial",
value: FileVaultProfileStatus.ENFORCING,
tooltip: "Setting will be enforced when the hosts come online.",
value: "enforcing",
tooltip:
"These hosts will receive the MDM command to install the disk encryption profile when the hosts come online.",
},
failed: {
displayName: "Failed",
statusName: "error",
value: FileVaultProfileStatus.FAILED,
value: "failed",
},
removing_enforcement: {
displayName: "Removing enforcement (pending)",
statusName: "pendingPartial",
value: FileVaultProfileStatus.REMOVING_ENFORCEMENT,
tooltip: "Enforcement will be removed when the hosts come online.",
value: "removing_enforcement",
tooltip:
"These hosts will receive the MDM command to remove the disk encryption profile when the hosts come online.",
},
};

View File

@ -53,6 +53,11 @@ export const getHostSelectStatuses = (isSandboxMode = false) => {
};
export const MAC_SETTINGS_FILTER_OPTIONS = [
{
disabled: false,
label: "Verified",
value: "verified",
},
{
disabled: false,
label: "Verifying",

View File

@ -9,30 +9,35 @@ import { FileVaultProfileStatus } from "interfaces/mdm";
const baseClass = "disk-encryption-status-filter";
const DISK_ENCRYPTION_STATUS_OPTIONS: IDropdownOption[] = [
{
disabled: false,
label: "Verified",
value: "verified",
},
{
disabled: false,
label: "Verifying",
value: FileVaultProfileStatus.VERIFYING,
value: "verifying",
},
{
disabled: false,
label: "Action required",
value: FileVaultProfileStatus.ACTION_REQUIRED,
value: "action_required",
},
{
disabled: false,
label: "Enforcing",
value: FileVaultProfileStatus.ENFORCING,
value: "enforcing",
},
{
disabled: false,
label: "Failed",
value: FileVaultProfileStatus.FAILED,
value: "failed",
},
{
disabled: false,
label: "Removing enforcement",
value: FileVaultProfileStatus.REMOVING_ENFORCEMENT,
value: "removing_enforcement",
},
];

View File

@ -411,7 +411,7 @@ const DeviceUserPage = ({
bootstrapPackageData={bootstrapPackageData}
isPremiumTier={isPremiumTier}
toggleMacSettingsModal={toggleMacSettingsModal}
hostMacSettings={host?.mdm.profiles ?? []}
hostMdmProfiles={host?.mdm.profiles ?? []}
mdmName={deviceMacAdminsData?.mobile_device_management?.name}
showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost}

View File

@ -42,6 +42,8 @@ import {
} from "utilities/helpers";
import permissions from "utilities/permissions";
import { createMockHostMacMdmProfile } from "__mocks__/hostMock";
import HostSummaryCard from "../cards/HostSummary";
import AboutCard from "../cards/About";
import AgentOptionsCard from "../cards/AgentOptions";
@ -693,7 +695,7 @@ const HostDetailsPage = ({
toggleOSPolicyModal={toggleOSPolicyModal}
toggleMacSettingsModal={toggleMacSettingsModal}
toggleBootstrapPackageModal={toggleBootstrapPackageModal}
hostMacSettings={host?.mdm.profiles ?? []}
hostMdmProfiles={host?.mdm.profiles ?? []}
mdmName={mdm?.name}
showRefetchSpinner={showRefetchSpinner}
onRefetchHost={onRefetchHost}

View File

@ -8,7 +8,6 @@ import { generateTableData } from "./MacSettingsTable/MacSettingsTableConfig";
interface IMacSettingsModalProps {
hostMDMData?: Pick<IHostMdmData, "profiles" | "macos_settings">;
isDeviceUser?: boolean;
onClose: () => void;
}

View File

@ -10,7 +10,11 @@ describe("Mac setting status cell", () => {
const operationType: MacMdmProfileOperationType = "install";
render(
<MacSettingStatusCell status={status} operationType={operationType} />
<MacSettingStatusCell
profileName="Test Profile"
status={status}
operationType={operationType}
/>
);
expect(screen.getByText("Verifying")).toBeInTheDocument();
@ -23,13 +27,17 @@ describe("Mac setting status cell", () => {
const customRender = createCustomRenderer();
const { user } = customRender(
<MacSettingStatusCell status={status} operationType={operationType} />
<MacSettingStatusCell
profileName="Test Profile"
status={status}
operationType={operationType}
/>
);
const statusText = screen.getByText("Verifying");
await user.hover(statusText);
expect(screen.getByText("Host applied the setting.")).toBeInTheDocument();
expect(screen.getByText(/verifying/)).toBeInTheDocument();
});
});

View File

@ -5,7 +5,10 @@ import { uniqueId } from "lodash";
import Icon from "components/Icon";
import { IconNames } from "components/icons";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { MacMdmProfileOperationType } from "interfaces/mdm";
import {
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
MacMdmProfileOperationType,
} from "interfaces/mdm";
import { MacSettingsTableStatusValue } from "../MacSettingsTableConfig";
import TooltipContent, {
@ -36,17 +39,36 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
pending: {
statusText: "Enforcing (pending)",
iconName: "pending-partial",
tooltip: "Setting will be enforced when the host comes online.", // TODO: this doesn't work for disk encryption or the device page generally
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host will receive the MDM command to install the disk encryption profile when the " +
"host comes online."
: "The host will receive the MDM command to install the configuration profile when the " +
"host comes online.",
},
action_required: {
statusText: "Action required (pending)",
iconName: "pending-partial",
tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
},
verified: {
statusText: "Verified",
iconName: "success",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host turned disk encryption on and " +
"sent their key to Fleet. Fleet verified with osquery."
: "The host installed the configuration profile. Fleet verified with osquery.",
},
verifying: {
statusText: "Verifying",
iconName: "success-partial",
tooltip: "Host applied the setting.",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host acknowledged the MDM command to install disk encryption profile. Fleet is " +
"verifying with osquery and retrieving the disk encryption key. This may take up to one hour."
: "The host acknowledged the MDM command to install the configuration profile. Fleet is " +
"verifying with osquery.",
},
failed: {
statusText: "Failed",
@ -58,9 +80,15 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
pending: {
statusText: "Removing enforcement (pending)",
iconName: "pending-partial",
tooltip: "Enforcement will be removed when the host comes online.",
tooltip: (innerProps) =>
innerProps.isDiskEncryptionProfile
? "The host will receive the MDM command to remove the disk encryption profile when the " +
"host comes online."
: "The host will receive the MDM command to remove the configuration profile when the host " +
"comes online.",
},
action_required: null, // should not be reached
verified: null, // should not be reached
verifying: null, // should not be reached
failed: {
statusText: "Failed",
@ -73,7 +101,7 @@ const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
interface IMacSettingStatusCellProps {
status: MacSettingsTableStatusValue;
operationType: MacMdmProfileOperationType;
profileName?: string;
profileName: string;
}
const MacSettingStatusCell = ({
@ -81,14 +109,17 @@ const MacSettingStatusCell = ({
operationType,
profileName = "",
}: IMacSettingStatusCellProps): JSX.Element => {
const options = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
// TODO: confirm this approach
const diplayOption = PROFILE_DISPLAY_CONFIG[operationType]?.[status];
const isDeviceUser = window.location.pathname
.toLowerCase()
.includes("/device/");
if (options) {
const { statusText, iconName, tooltip } = options;
const isDiskEncryptionProfile =
profileName === FLEET_FILEVAULT_PROFILE_DISPLAY_NAME;
if (diplayOption) {
const { statusText, iconName, tooltip } = diplayOption;
const tooltipId = uniqueId();
return (
<span className={baseClass}>
@ -111,10 +142,19 @@ const MacSettingStatusCell = ({
data-html
>
<span className="tooltip__tooltip-text">
<TooltipContent
innerContent={tooltip}
innerProps={{ isDeviceUser, profileName }}
/>
{status !== "action_required" ? (
<TooltipContent
innerContent={tooltip}
innerProps={{
isDiskEncryptionProfile,
}}
/>
) : (
<TooltipContent
innerContent={tooltip}
innerProps={{ isDeviceUser, profileName }}
/>
)}
</span>
</ReactTooltip>
</>

View File

@ -40,7 +40,7 @@ interface IHostSummaryProps {
toggleOSPolicyModal?: () => void;
toggleMacSettingsModal?: () => void;
toggleBootstrapPackageModal?: () => void;
hostMacSettings?: IHostMacMdmProfile[];
hostMdmProfiles?: IHostMacMdmProfile[];
mdmName?: string;
showRefetchSpinner: boolean;
onRefetchHost: (
@ -60,7 +60,7 @@ const HostSummary = ({
toggleOSPolicyModal,
toggleMacSettingsModal,
toggleBootstrapPackageModal,
hostMacSettings,
hostMdmProfiles,
mdmName,
showRefetchSpinner,
onRefetchHost,
@ -155,6 +155,7 @@ const HostSummary = ({
const renderSummary = () => {
const { status, id } = titleData;
return (
<div className="info-flex">
<div className="info-flex__item info-flex__item--title">
@ -178,11 +179,11 @@ const HostSummary = ({
{titleData.platform === "darwin" &&
isPremiumTier &&
mdmName === "Fleet" && // show if 1 - host is enrolled in Fleet MDM, and
hostMacSettings &&
hostMacSettings.length > 0 && ( // 2 - host has at least one setting (profile) enforced
hostMdmProfiles &&
hostMdmProfiles.length > 0 && ( // 2 - host has at least one setting (profile) enforced
<HostSummaryIndicator title="macOS settings">
<MacSettingsIndicator
profiles={hostMacSettings}
profiles={hostMdmProfiles}
onClick={toggleMacSettingsModal}
/>
</HostSummaryIndicator>

View File

@ -9,7 +9,7 @@ import { IconNames } from "components/icons";
const baseClass = "mac-settings-indicator";
type MacSettingsStatus = "Failing" | "Verifying" | "Pending";
type MacProfileStatus = "Failed" | "Verifying" | "Pending" | "Verified";
interface IStatusDisplayOption {
iconName: Extract<
@ -18,35 +18,53 @@ interface IStatusDisplayOption {
>;
tooltipText: string;
}
type StatusDisplayOptions = Record<MacSettingsStatus, IStatusDisplayOption>;
type StatusDisplayOptions = Record<MacProfileStatus, IStatusDisplayOption>;
const STATUS_DISPLAY_OPTIONS: StatusDisplayOptions = {
Verified: {
iconName: "success",
tooltipText:
"The host installed all configuration profiles. Fleet verified with osquery.",
},
Verifying: {
iconName: "success-partial",
tooltipText: "Host applied the latest settings",
tooltipText:
"The hosts acknowledged all MDM commands to install configuration profiles. Fleet is verifying " +
"the profiles are installed with osquery.",
},
Pending: {
iconName: "pending-partial",
tooltipText: "Host will apply the latest settings when it comes online",
tooltipText:
"The host will receive MDM commands to install configuration profiles when the host come online.",
},
Failing: {
Failed: {
iconName: "error",
tooltipText:
"Host failed to apply the latest settings. Click to view error(s).",
"Host failed to install configuration profiles. Click to view error(s).",
},
};
const getMacSettingsStatus = (
hostMacSettings?: IHostMacMdmProfile[]
): MacSettingsStatus => {
const statuses = hostMacSettings?.map((setting) => setting.status);
if (statuses?.includes("failed")) {
return "Failing";
/**
* Returns the displayed status of the macOS settings field based on the
* profile statuses.
* If any profile has a status of "failed", the status will be displayed as "Failed" and
* continues to fall through to "Pending" and "Verifying" if any profiles have those statuses.
* Finally if all profiles have a status of "verified", the status will be displayed as "Verified".
*/
const getMacProfileStatus = (
hostMacSettings: IHostMacMdmProfile[]
): MacProfileStatus => {
const statuses = hostMacSettings.map((setting) => setting.status);
if (statuses.includes("failed")) {
return "Failed";
}
if (statuses?.includes("pending")) {
if (statuses.includes("pending")) {
return "Pending";
}
return "Verifying";
if (statuses.includes("verifying")) {
return "Verifying";
}
return "Verified";
};
interface IMacSettingsIndicatorProps {
@ -57,14 +75,13 @@ const MacSettingsIndicator = ({
profiles,
onClick,
}: IMacSettingsIndicatorProps): JSX.Element => {
const macSettingsStatus = getMacSettingsStatus(profiles);
const macProfileStatus = getMacProfileStatus(profiles);
const iconName = STATUS_DISPLAY_OPTIONS[macSettingsStatus].iconName;
const tooltipText = STATUS_DISPLAY_OPTIONS[macSettingsStatus].tooltipText;
const statusDisplayOption = STATUS_DISPLAY_OPTIONS[macProfileStatus];
return (
<span className={`${baseClass} info-flex__data`}>
<Icon name={iconName} />
<Icon name={statusDisplayOption.iconName} />
<span
className="tooltip tooltip__tooltip-icon"
data-tip
@ -76,7 +93,7 @@ const MacSettingsIndicator = ({
variant="text-link"
className={`${baseClass}__button`}
>
{macSettingsStatus}
{macProfileStatus}
</Button>
</span>
<ReactTooltip
@ -86,7 +103,9 @@ const MacSettingsIndicator = ({
id={`${baseClass}-tooltip`}
data-html
>
<span className="tooltip__tooltip-text">{tooltipText}</span>
<span className="tooltip__tooltip-text">
{statusDisplayOption.tooltipText}
</span>
</ReactTooltip>
</span>
);

View File

@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { FileVaultProfileStatus } from "interfaces/mdm";
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url";
export type IFileVaultSummaryResponse = Record<FileVaultProfileStatus, number>;
export interface IEulaMetadataResponse {
name: string;
token: string;