mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
simplify OS modal (#43252)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40702 New look: <img width="812" height="350" alt="image" src="https://github.com/user-attachments/assets/83e82480-b756-4c51-be3f-09a72e736770" /> # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Simplified pending status labels in OS Settings modal by removing "(pending)" suffix from states like "Enforcing" and "Removing enforcement" * Improved OS Settings modal table layout and styling * **New Features** * Added dedicated action buttons to resend MDM profiles and rotate Recovery Lock password * Enhanced error tooltip handling for failed profile states <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
0cc037f80d
commit
90f75f1644
16 changed files with 529 additions and 544 deletions
1
changes/40702-simplif-os-modal
Normal file
1
changes/40702-simplif-os-modal
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improved the OS settings modal layout.
|
||||
|
|
@ -161,7 +161,7 @@ const STATUS_CELL_VALUES: Record<DiskEncryptionStatus, IStatusCellValue> = {
|
|||
"osquery and retrieving the disk encryption key. This may take up to one hour.",
|
||||
},
|
||||
action_required: {
|
||||
displayName: "Action required (pending)",
|
||||
displayName: "Action required",
|
||||
statusName: "pendingPartial",
|
||||
value: "action_required",
|
||||
tooltip: (
|
||||
|
|
@ -172,7 +172,7 @@ const STATUS_CELL_VALUES: Record<DiskEncryptionStatus, IStatusCellValue> = {
|
|||
),
|
||||
},
|
||||
enforcing: {
|
||||
displayName: "Enforcing (pending)",
|
||||
displayName: "Enforcing",
|
||||
statusName: "pendingPartial",
|
||||
value: "enforcing",
|
||||
tooltip:
|
||||
|
|
@ -184,7 +184,7 @@ const STATUS_CELL_VALUES: Record<DiskEncryptionStatus, IStatusCellValue> = {
|
|||
value: "failed",
|
||||
},
|
||||
removing_enforcement: {
|
||||
displayName: "Removing enforcement (pending)",
|
||||
displayName: "Removing enforcement",
|
||||
statusName: "pendingPartial",
|
||||
value: "removing_enforcement",
|
||||
tooltip:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ describe("OS setting status cell", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText("Enforcing (pending)");
|
||||
const statusText = screen.getByText("Enforcing");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
|
||||
await user.hover(statusText);
|
||||
|
|
@ -82,7 +82,7 @@ describe("OS setting status cell", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText("Enforcing (pending)");
|
||||
const statusText = screen.getByText("Enforcing");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
|
||||
await user.hover(statusText);
|
||||
|
|
@ -105,7 +105,7 @@ describe("OS setting status cell", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText("Enforcing (pending)");
|
||||
const statusText = screen.getByText("Enforcing");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
|
||||
await user.hover(statusText);
|
||||
|
|
@ -128,7 +128,7 @@ describe("OS setting status cell", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText("Removing enforcement (pending)");
|
||||
const statusText = screen.getByText("Removing enforcement");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
|
||||
await user.hover(statusText);
|
||||
|
|
@ -151,7 +151,7 @@ describe("OS setting status cell", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const statusText = screen.getByText("Removing enforcement (pending)");
|
||||
const statusText = screen.getByText("Removing enforcement");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
|
||||
await user.hover(statusText);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@ import {
|
|||
} from "interfaces/mdm";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
import { OsSettingsTableStatusValue } from "../OSSettingsTableConfig";
|
||||
import {
|
||||
IHostMdmProfileWithAddedStatus,
|
||||
OsSettingsTableStatusValue,
|
||||
} from "../OSSettingsTableConfig";
|
||||
import TooltipContent from "./components/Tooltip/TooltipContent";
|
||||
import generateErrorTooltip from "./errorTooltipHelpers";
|
||||
import {
|
||||
isDiskEncryptionProfile,
|
||||
LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG,
|
||||
|
|
@ -34,6 +38,7 @@ interface IOSSettingStatusCellProps {
|
|||
profileName: string;
|
||||
hostPlatform?: ProfilePlatform;
|
||||
profileUUID?: string;
|
||||
profile?: IHostMdmProfileWithAddedStatus;
|
||||
}
|
||||
|
||||
const OSSettingStatusCell = ({
|
||||
|
|
@ -42,6 +47,7 @@ const OSSettingStatusCell = ({
|
|||
profileName = "",
|
||||
hostPlatform,
|
||||
profileUUID,
|
||||
profile,
|
||||
}: IOSSettingStatusCellProps) => {
|
||||
let displayOption: ProfileDisplayOption = null;
|
||||
if (hostPlatform === "linux") {
|
||||
|
|
@ -65,14 +71,14 @@ const OSSettingStatusCell = ({
|
|||
case "delivered":
|
||||
if (operationType === "install") {
|
||||
displayOption = {
|
||||
statusText: "Enforcing (pending)",
|
||||
statusText: "Enforcing",
|
||||
iconName: "pending-outline",
|
||||
tooltip:
|
||||
"The host is running the command to apply settings or will run it when the host comes online.",
|
||||
};
|
||||
} else {
|
||||
displayOption = {
|
||||
statusText: "Removing enforcement (pending)",
|
||||
statusText: "Removing enforcement",
|
||||
iconName: "pending-outline",
|
||||
tooltip:
|
||||
"The host is running the command to remove settings or will run it when the host comes online.",
|
||||
|
|
@ -120,34 +126,50 @@ const OSSettingStatusCell = ({
|
|||
|
||||
if (displayOption) {
|
||||
const { statusText, iconName, tooltip } = displayOption;
|
||||
|
||||
// For failed status, use the error detail as tooltip content
|
||||
const errorTooltip = profile ? generateErrorTooltip(profile) : null;
|
||||
|
||||
let tipContent: React.ReactNode;
|
||||
if (tooltip) {
|
||||
if (status !== "action_required") {
|
||||
tipContent = (
|
||||
<span className="tooltip__tooltip-text">
|
||||
<TooltipContent
|
||||
innerContent={tooltip}
|
||||
innerProps={{
|
||||
isDiskEncryptionProfile: isDiskEncryptionProfile(profileName),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
tipContent = (
|
||||
<span className="tooltip__tooltip-text">
|
||||
<TooltipContent
|
||||
innerContent={tooltip}
|
||||
innerProps={{ isDeviceUser, profileName }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else if (errorTooltip) {
|
||||
tipContent = (
|
||||
<span className="tooltip__tooltip-text">{errorTooltip}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={baseClass}>
|
||||
<Icon name={iconName} />
|
||||
{tooltip ? (
|
||||
{tipContent ? (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<span className="tooltip__tooltip-text">
|
||||
{status !== "action_required" ? (
|
||||
<TooltipContent
|
||||
innerContent={tooltip}
|
||||
innerProps={{
|
||||
isDiskEncryptionProfile: isDiskEncryptionProfile(
|
||||
profileName
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TooltipContent
|
||||
innerContent={tooltip}
|
||||
innerProps={{ isDeviceUser, profileName }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
tipContent={tipContent}
|
||||
position="top"
|
||||
underline={false}
|
||||
showArrow
|
||||
tipOffset={8}
|
||||
clickable
|
||||
>
|
||||
<span className={`${baseClass}__status-text`}>{statusText}</span>
|
||||
</TooltipWrapper>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { createMockHostMdmProfile } from "__mocks__/hostMock";
|
||||
|
||||
import generateErrorTooltip from "./errorTooltipHelpers";
|
||||
|
||||
// Helper to render the JSX returned by generateErrorTooltip
|
||||
const renderTooltip = (tooltip: React.ReactNode) => {
|
||||
return render(<div>{tooltip}</div>);
|
||||
};
|
||||
|
||||
describe("generateErrorTooltip", () => {
|
||||
it("returns null for non-failed profiles", () => {
|
||||
const result = generateErrorTooltip(
|
||||
createMockHostMdmProfile({ status: "verified" })
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for failed profiles with no detail", () => {
|
||||
const result = generateErrorTooltip(
|
||||
createMockHostMdmProfile({ status: "failed", detail: "" })
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("formats a windows profile error with key-value pairs", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
platform: "windows",
|
||||
status: "failed",
|
||||
detail:
|
||||
"starting encryption: encrypt(C:): error code returned during encryption: -2147024809, error 2: This is another error",
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
const firstErrorKey = screen.getByText(
|
||||
(content) => content === "starting encryption:"
|
||||
);
|
||||
const firstErrorValue = screen.getByText(
|
||||
(content) =>
|
||||
content ===
|
||||
"encrypt(C:): error code returned during encryption: -2147024809,"
|
||||
);
|
||||
|
||||
expect(firstErrorKey).toBeInTheDocument();
|
||||
expect(firstErrorKey.tagName.toLowerCase()).toBe("b");
|
||||
expect(firstErrorValue).toBeInTheDocument();
|
||||
|
||||
const secondErrorKey = screen.getByText(
|
||||
(content) => content === "error 2:"
|
||||
);
|
||||
const secondErrorValue = screen.getByText(
|
||||
(content) => content === "This is another error"
|
||||
);
|
||||
|
||||
expect(secondErrorKey).toBeInTheDocument();
|
||||
expect(secondErrorKey.tagName.toLowerCase()).toBe("b");
|
||||
expect(secondErrorValue).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a tooltip link for IDP email errors", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: "There is no IdP email for this host.",
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
expect(screen.getByText(/Learn more/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Learn more/).tagName.toLowerCase()).toBe("a");
|
||||
});
|
||||
|
||||
it("formats a custom SCEP certificate error", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Fleet couldn't populate $FLEET_VAR_CUSTOM_SCEP_URL_SCEP_WIFI because SCEP_WIFI certificate authority doesn't exist.`,
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/add it and resend the configuration profile/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("formats a DigiCert profile ID error (410)", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert for WIFI_CERTIFICATE. unexpected DigiCert status code for POST request: 410, errors: Profile with id {test-id} was deleted`,
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("WIFI_CERTIFICATE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Profile GUID")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("formats a DigiCert deleted/suspended profile error (400)", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert for WIFI_CERTIFICATE. unexpected DigiCert status code for POST request: 400, errors: Enrollment creation and Certificate issuance/renewal for deleted or suspended Profile are not supported.
|
||||
Please contact system Administrator.`,
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("WIFI_CERTIFICATE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Profile GUID")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("formats a DigiCert token error", () => {
|
||||
const tooltip = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert. The API token configured in DIGICERT_TEST certificate authority is invalid.`,
|
||||
})
|
||||
);
|
||||
|
||||
renderTooltip(tooltip);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("DIGICERT_TEST")).toBeInTheDocument();
|
||||
expect(screen.getByText("API token")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("returns the raw detail string for unrecognized darwin errors", () => {
|
||||
const result = generateErrorTooltip(
|
||||
createMockHostMdmProfile({
|
||||
platform: "darwin",
|
||||
status: "failed",
|
||||
detail: "Some unknown error occurred",
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toBe("Some unknown error occurred");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,54 +1,10 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers";
|
||||
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import {
|
||||
FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID,
|
||||
IHostMdmProfile,
|
||||
} from "interfaces/mdm";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { IHostMdmProfile } from "interfaces/mdm";
|
||||
|
||||
import { IHostMdmProfileWithAddedStatus } from "../OSSettingsTableConfig";
|
||||
|
||||
const baseClass = "os-settings-error-cell";
|
||||
|
||||
interface IResendButtonProps {
|
||||
isResending: boolean;
|
||||
onClick: (evt: React.MouseEvent<HTMLButtonElement, React.MouseEvent>) => void;
|
||||
}
|
||||
|
||||
const ResendButton = ({ isResending, onClick }: IResendButtonProps) => {
|
||||
const classNames = classnames(`${baseClass}__resend-button`, "resend-link", {
|
||||
[`${baseClass}__resending`]: isResending,
|
||||
});
|
||||
|
||||
const buttonText = isResending ? "Resending..." : "Resend";
|
||||
|
||||
// add additional props when we need to display a tooltip for the button
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isResending}
|
||||
onClick={onClick}
|
||||
variant="inverse"
|
||||
className={classNames}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="refresh" color="ui-fleet-black-75" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const formatAndroidProfileNotAppliedError = (
|
||||
detail: IHostMdmProfile["detail"]
|
||||
) => {
|
||||
|
|
@ -207,10 +163,9 @@ const formatDetailWindowsProfile = (detail: string) => {
|
|||
* unformatted.
|
||||
*/
|
||||
const generateErrorTooltip = (
|
||||
cellValue: string,
|
||||
profile: IHostMdmProfileWithAddedStatus
|
||||
) => {
|
||||
if (profile.status !== "failed") return null;
|
||||
): React.ReactNode => {
|
||||
if (profile.status !== "failed" || !profile.detail) return null;
|
||||
|
||||
// Special case to handle IdP email errors
|
||||
const idpEmailError = formatDetailIdpEmailError(profile.detail);
|
||||
|
|
@ -235,141 +190,7 @@ const generateErrorTooltip = (
|
|||
return formatDetailWindowsProfile(profile.detail);
|
||||
}
|
||||
|
||||
return cellValue;
|
||||
return profile.detail;
|
||||
};
|
||||
|
||||
interface IRotateButtonProps {
|
||||
isRotating: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const RotateButton = ({ isRotating, onClick }: IRotateButtonProps) => {
|
||||
const classNames = classnames(`${baseClass}__rotate-button`, "rotate-link", {
|
||||
[`${baseClass}__rotating`]: isRotating,
|
||||
});
|
||||
|
||||
const buttonText = isRotating ? "Rotating..." : "Rotate";
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isRotating}
|
||||
onClick={onClick}
|
||||
variant="inverse"
|
||||
className={classNames}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="refresh" color="ui-fleet-black-75" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IOSSettingsErrorCellProps {
|
||||
canResendProfiles: boolean;
|
||||
canRotateRecoveryLockPassword?: boolean;
|
||||
profile: IHostMdmProfileWithAddedStatus;
|
||||
resendRequest: (profileUUID: string) => Promise<void>;
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>;
|
||||
rotateRecoveryLockPassword?: () => Promise<void>;
|
||||
onProfileResent?: () => void;
|
||||
}
|
||||
|
||||
const OSSettingsErrorCell = ({
|
||||
canResendProfiles,
|
||||
canRotateRecoveryLockPassword = false,
|
||||
profile,
|
||||
resendRequest,
|
||||
resendCertificateRequest,
|
||||
rotateRecoveryLockPassword,
|
||||
onProfileResent = noop,
|
||||
}: IOSSettingsErrorCellProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isResending, setIsResending] = useState(false);
|
||||
const [isRotating, setIsRotating] = useState(false);
|
||||
|
||||
const isAndroidCertificate =
|
||||
profile.profile_uuid === FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID;
|
||||
|
||||
const onResendProfile = async () => {
|
||||
setIsResending(true);
|
||||
try {
|
||||
if (
|
||||
isAndroidCertificate &&
|
||||
resendCertificateRequest &&
|
||||
profile.certificate_template_id !== undefined
|
||||
) {
|
||||
await resendCertificateRequest(profile.certificate_template_id);
|
||||
renderFlash(
|
||||
"success",
|
||||
"Successfully sent request to resend certificate."
|
||||
);
|
||||
onProfileResent();
|
||||
} else if (!isAndroidCertificate) {
|
||||
await resendRequest(profile.profile_uuid);
|
||||
onProfileResent();
|
||||
}
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn't resend. Please try again.");
|
||||
}
|
||||
setIsResending(false);
|
||||
};
|
||||
|
||||
const onRotatePassword = async () => {
|
||||
if (!rotateRecoveryLockPassword) return;
|
||||
setIsRotating(true);
|
||||
try {
|
||||
await rotateRecoveryLockPassword();
|
||||
renderFlash(
|
||||
"success",
|
||||
"Successfully sent request to rotate Recovery Lock password."
|
||||
);
|
||||
} catch (e) {
|
||||
const msg = getErrorReason(e).includes("already in progress")
|
||||
? "Recovery lock password rotation is already in progress for this host."
|
||||
: "Couldn't send request to rotate Recovery Lock password. Please try again.";
|
||||
|
||||
renderFlash("error", msg);
|
||||
}
|
||||
setIsRotating(false);
|
||||
};
|
||||
|
||||
const isFailed = profile.status === "failed";
|
||||
const isPending =
|
||||
profile.status === "pending" ||
|
||||
profile.status === "delivering" ||
|
||||
profile.status === "delivered";
|
||||
const isVerified = profile.status === "verified";
|
||||
const showResendButton =
|
||||
canResendProfiles &&
|
||||
(isFailed || isVerified) &&
|
||||
profile.profile_uuid !== REC_LOCK_SYNTHETIC_PROFILE_UUID;
|
||||
const showRotateButton =
|
||||
canRotateRecoveryLockPassword && (isFailed || isVerified);
|
||||
const value =
|
||||
((isFailed || isPending) && profile.detail) || DEFAULT_EMPTY_CELL_VALUE;
|
||||
|
||||
const tooltip = generateErrorTooltip(value, profile);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TooltipTruncatedTextCell
|
||||
tooltipBreakOnWord
|
||||
tooltip={tooltip}
|
||||
value={value}
|
||||
className={
|
||||
isFailed || showResendButton || showRotateButton
|
||||
? `${baseClass}__failed-message`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{showResendButton && (
|
||||
<ResendButton isResending={isResending} onClick={onResendProfile} />
|
||||
)}
|
||||
{showRotateButton && (
|
||||
<RotateButton isRotating={isRotating} onClick={onRotatePassword} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OSSettingsErrorCell;
|
||||
export default generateErrorTooltip;
|
||||
|
|
@ -64,7 +64,7 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
|||
verifying: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG,
|
||||
acknowledged: MAC_PROFILE_VERIFYING_DISPLAY_CONFIG,
|
||||
pending: {
|
||||
statusText: "Enforcing (pending)",
|
||||
statusText: "Enforcing",
|
||||
iconName: "pending-outline",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
|
|
@ -74,7 +74,7 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
|||
"when the host comes online.",
|
||||
},
|
||||
action_required: {
|
||||
statusText: "Action required (pending)",
|
||||
statusText: "Action required",
|
||||
iconName: "pending-outline",
|
||||
tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
|
||||
},
|
||||
|
|
@ -86,7 +86,7 @@ export const PROFILE_DISPLAY_CONFIG: ProfileDisplayConfig = {
|
|||
},
|
||||
remove: {
|
||||
pending: {
|
||||
statusText: "Removing enforcement (pending)",
|
||||
statusText: "Removing enforcement",
|
||||
iconName: "pending-outline",
|
||||
tooltip: (innerProps) =>
|
||||
innerProps.isDiskEncryptionProfile
|
||||
|
|
@ -133,13 +133,13 @@ export const WINDOWS_DISK_ENCRYPTION_DISPLAY_CONFIG: WindowsDiskEncryptionDispla
|
|||
"osquery and retrieving the disk encryption key. This may take up to one hour.",
|
||||
},
|
||||
pending: {
|
||||
statusText: "Enforcing (pending)",
|
||||
statusText: "Enforcing",
|
||||
iconName: "pending-outline",
|
||||
tooltip: () =>
|
||||
"The host will receive the MDM command to turn on disk encryption when the host comes online.",
|
||||
},
|
||||
action_required: {
|
||||
statusText: "Action required (pending)",
|
||||
statusText: "Action required",
|
||||
iconName: "pending-outline",
|
||||
tooltip: () =>
|
||||
"Disk encryption is on, but the user has not set a BitLocker PIN yet.",
|
||||
|
|
@ -169,7 +169,7 @@ export const LINUX_DISK_ENCRYPTION_DISPLAY_CONFIG: LinuxDiskEncryptionDisplayCon
|
|||
tooltip: null,
|
||||
},
|
||||
action_required: {
|
||||
statusText: "Action required (pending)",
|
||||
statusText: "Action required",
|
||||
iconName: "pending-outline",
|
||||
tooltip: TooltipInnerContentActionRequired as TooltipInnerContentFunc,
|
||||
},
|
||||
|
|
@ -185,12 +185,12 @@ export const RECOVERY_LOCK_PASSWORD_DISPLAY_CONFIG: Record<
|
|||
tooltip: "Fleet set a recovery lock password for the host.",
|
||||
},
|
||||
pending: {
|
||||
statusText: "Enforcing (pending)",
|
||||
statusText: "Enforcing",
|
||||
iconName: "pending-outline",
|
||||
tooltip: "Fleet is setting a recovery lock password for the host.",
|
||||
},
|
||||
removing_enforcement: {
|
||||
statusText: "Removing enforcement (pending)",
|
||||
statusText: "Removing enforcement",
|
||||
iconName: "pending-outline",
|
||||
tooltip: "Fleet is unsetting the recovery lock password for the host.",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,278 +0,0 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { createMockHostMdmProfile } from "__mocks__/hostMock";
|
||||
|
||||
import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers";
|
||||
|
||||
import OSSettingsErrorCell from "./OSSettingsErrorCell";
|
||||
|
||||
const noop = () => new Promise<void>(() => undefined);
|
||||
|
||||
describe("OSSettingsErrorCell", () => {
|
||||
it("should render a formatted message for windows profiles", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
platform: "windows",
|
||||
status: "failed",
|
||||
detail:
|
||||
"starting encryption: encrypt(C:): error code returned during encryption: -2147024809, error 2: This is another error",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
const firstErrorKey = screen.getByText(
|
||||
(content) => content === "starting encryption:"
|
||||
);
|
||||
const firstErrorValue = screen.getByText(
|
||||
(content) =>
|
||||
content ===
|
||||
"encrypt(C:): error code returned during encryption: -2147024809,"
|
||||
);
|
||||
|
||||
// assert that the tooltip errors are rendered and the key is bolded
|
||||
expect(firstErrorKey).toBeInTheDocument();
|
||||
expect(firstErrorKey.tagName.toLowerCase()).toBe("b");
|
||||
expect(firstErrorValue).toBeInTheDocument();
|
||||
|
||||
const secondErrorKey = screen.getByText(
|
||||
(content) => content === "error 2:"
|
||||
);
|
||||
const secondErrorValue = screen.getByText(
|
||||
(content) => content === "This is another error"
|
||||
);
|
||||
|
||||
// assert the second error is rendered with the key bolded
|
||||
expect(secondErrorKey).toBeInTheDocument();
|
||||
expect(secondErrorKey.tagName.toLowerCase()).toBe("b");
|
||||
expect(secondErrorValue).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a default empty cell when the status is not failed", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getAllByText("---")[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a resend button when canResendProfiles is true and profile is failed", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({ status: "failed" })}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Resend" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a resend button when canResendProfiles is true and profile is verified", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({ status: "verified" })}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Resend" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a tooltip link when the error message inlcudes info about IDP emails", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: "There is no IdP email for this host.",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
// couldnt get getByRole to work for this link. Thinking it may be a jest issue
|
||||
// TODO: explore why getByRole is not working for links
|
||||
expect(screen.getByText(/Learn more/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Learn more/).tagName.toLowerCase()).toBe("a");
|
||||
});
|
||||
|
||||
it("renders a formatted tooltip when the error message matches custom scep error patern", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Fleet couldn't populate $FLEET_VAR_CUSTOM_SCEP_URL_SCEP_WIFI because SCEP_WIFI certificate authority doesn't exist.`,
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/add it and resend the configuration profile/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a formatted tooltip when the error message matches digicert profile id error", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert for WIFI_CERTIFICATE. unexpected DigiCert status code for POST request: 410, errors: Profile with id {test-id} was deleted`,
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("WIFI_CERTIFICATE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Profile GUID")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a formatted tooltip when the error message matches digicert deleted profile error", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert for WIFI_CERTIFICATE. unexpected DigiCert status code for POST request: 400, errors: Enrollment creation and Certificate issuance/renewal for deleted or suspended Profile are not supported.
|
||||
Please contact system Administrator.`,
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("WIFI_CERTIFICATE")).toBeInTheDocument();
|
||||
expect(screen.getByText("Profile GUID")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is verified", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "verified",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is failed", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "failed",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render a rotate button when canRotateRecoveryLockPassword is false", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "verified",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Rotate" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render a rotate button when password status is pending", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "pending",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Rotate" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a formatted tooltip when the error message matches digicert token patern", () => {
|
||||
render(
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
status: "failed",
|
||||
detail: `Couldn't get certificate from DigiCert. The API token configured in DIGICERT_TEST certificate authority is invalid.`,
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Settings > Integrations > Certificates")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/correct it and resend/)).toBeInTheDocument();
|
||||
expect(screen.getByText("DIGICERT_TEST")).toBeInTheDocument();
|
||||
expect(screen.getByText("API token")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./OSSettingsErrorCell";
|
||||
|
|
@ -7,8 +7,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
// need to do some overrides here to get the name text content to be the right size
|
||||
.data-table-block .data-table tbody td .os-settings-name-cell__name-tooltip {
|
||||
max-width: 142px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { createMockHostMdmProfile } from "__mocks__/hostMock";
|
||||
|
||||
import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers";
|
||||
|
||||
import OSSettingsResendCell from "./OSSettingsResendCell";
|
||||
|
||||
const noop = () => Promise.resolve();
|
||||
|
||||
describe("OSSettingsResendCell", () => {
|
||||
it("renders a resend button when canResendProfiles is true and profile is failed", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({ status: "failed" })}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Resend" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a resend button when canResendProfiles is true and profile is verified", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({ status: "verified" })}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Resend" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is verified", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "verified",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is failed", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "failed",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render a rotate button when canRotateRecoveryLockPassword is false", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword={false}
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "verified",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Rotate" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render a rotate button when password status is pending", () => {
|
||||
render(
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles={false}
|
||||
canRotateRecoveryLockPassword
|
||||
profile={createMockHostMdmProfile({
|
||||
profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID,
|
||||
status: "pending",
|
||||
})}
|
||||
resendRequest={noop}
|
||||
rotateRecoveryLockPassword={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Rotate" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID } from "interfaces/mdm";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import { IHostMdmProfileWithAddedStatus } from "../OSSettingsTableConfig";
|
||||
|
||||
const baseClass = "os-settings-resend-cell";
|
||||
|
||||
interface IResendButtonProps {
|
||||
isResending: boolean;
|
||||
onClick: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
const ResendButton = ({ isResending, onClick }: IResendButtonProps) => {
|
||||
const classNames = classnames(`${baseClass}__resend-button`, "resend-link", {
|
||||
[`${baseClass}__resending`]: isResending,
|
||||
});
|
||||
|
||||
const buttonText = isResending ? "Resending..." : "Resend";
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isResending}
|
||||
onClick={onClick}
|
||||
variant="inverse"
|
||||
className={classNames}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="refresh" color="ui-fleet-black-75" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IRotateButtonProps {
|
||||
isRotating: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const RotateButton = ({ isRotating, onClick }: IRotateButtonProps) => {
|
||||
const classNames = classnames(`${baseClass}__rotate-button`, "rotate-link", {
|
||||
[`${baseClass}__rotating`]: isRotating,
|
||||
});
|
||||
|
||||
const buttonText = isRotating ? "Rotating..." : "Rotate";
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isRotating}
|
||||
onClick={onClick}
|
||||
variant="inverse"
|
||||
className={classNames}
|
||||
size="small"
|
||||
>
|
||||
<Icon name="refresh" color="ui-fleet-black-75" size="small" />
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IOSSettingsResendCellProps {
|
||||
canResendProfiles: boolean;
|
||||
canRotateRecoveryLockPassword?: boolean;
|
||||
profile: IHostMdmProfileWithAddedStatus;
|
||||
resendRequest: (profileUUID: string) => Promise<void>;
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>;
|
||||
rotateRecoveryLockPassword?: () => Promise<void>;
|
||||
onProfileResent?: () => void;
|
||||
}
|
||||
|
||||
const OSSettingsResendCell = ({
|
||||
canResendProfiles,
|
||||
canRotateRecoveryLockPassword = false,
|
||||
profile,
|
||||
resendRequest,
|
||||
resendCertificateRequest,
|
||||
rotateRecoveryLockPassword,
|
||||
onProfileResent = noop,
|
||||
}: IOSSettingsResendCellProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isResending, setIsResending] = useState(false);
|
||||
const [isRotating, setIsRotating] = useState(false);
|
||||
|
||||
const isAndroidCertificate =
|
||||
profile.profile_uuid === FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID;
|
||||
|
||||
const onResendProfile = async () => {
|
||||
setIsResending(true);
|
||||
try {
|
||||
if (
|
||||
isAndroidCertificate &&
|
||||
resendCertificateRequest &&
|
||||
profile.certificate_template_id !== undefined
|
||||
) {
|
||||
await resendCertificateRequest(profile.certificate_template_id);
|
||||
renderFlash(
|
||||
"success",
|
||||
"Successfully sent request to resend certificate."
|
||||
);
|
||||
onProfileResent();
|
||||
} else if (!isAndroidCertificate) {
|
||||
await resendRequest(profile.profile_uuid);
|
||||
onProfileResent();
|
||||
}
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn't resend. Please try again.");
|
||||
}
|
||||
setIsResending(false);
|
||||
};
|
||||
|
||||
const onRotatePassword = async () => {
|
||||
if (!rotateRecoveryLockPassword) return;
|
||||
setIsRotating(true);
|
||||
try {
|
||||
await rotateRecoveryLockPassword();
|
||||
renderFlash(
|
||||
"success",
|
||||
"Successfully sent request to rotate Recovery Lock password."
|
||||
);
|
||||
} catch (e) {
|
||||
const msg = getErrorReason(e).includes("already in progress")
|
||||
? "Recovery lock password rotation is already in progress for this host."
|
||||
: "Couldn't send request to rotate Recovery Lock password. Please try again.";
|
||||
|
||||
renderFlash("error", msg);
|
||||
}
|
||||
setIsRotating(false);
|
||||
};
|
||||
|
||||
const isFailed = profile.status === "failed";
|
||||
const isVerified = profile.status === "verified";
|
||||
const showResendButton =
|
||||
canResendProfiles &&
|
||||
(isFailed || isVerified) &&
|
||||
profile.profile_uuid !== REC_LOCK_SYNTHETIC_PROFILE_UUID;
|
||||
const showRotateButton =
|
||||
canRotateRecoveryLockPassword && (isFailed || isVerified);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{showResendButton && (
|
||||
<ResendButton isResending={isResending} onClick={onResendProfile} />
|
||||
)}
|
||||
{showRotateButton && (
|
||||
<RotateButton isRotating={isRotating} onClick={onRotatePassword} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OSSettingsResendCell;
|
||||
|
|
@ -1,21 +1,9 @@
|
|||
.os-settings-error-cell {
|
||||
.os-settings-resend-cell {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
|
||||
&__failed-message {
|
||||
// allows the message to shrink and not push the
|
||||
// resend button outside of the table cell
|
||||
min-width: 0;
|
||||
|
||||
.data-table__tooltip-truncated-text--cell {
|
||||
// for some reason this is need to vertically align the text and
|
||||
// the resend button
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__resend-button {
|
||||
width: 106px;
|
||||
display: flex;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./OSSettingsResendCell";
|
||||
|
|
@ -17,7 +17,7 @@ import { isAppleDevice, isIPadOrIPhone } from "interfaces/platform";
|
|||
|
||||
import OSSettingsNameCell from "./OSSettingsNameCell";
|
||||
import OSSettingStatusCell from "./OSSettingStatusCell";
|
||||
import OSSettingsErrorCell from "./OSSettingsErrorCell";
|
||||
import OSSettingsResendCell from "./OSSettingsResendCell";
|
||||
|
||||
import {
|
||||
generateLinuxDiskEncryptionSetting,
|
||||
|
|
@ -85,12 +85,13 @@ const generateTableConfig = (
|
|||
profileName={cellProps.row.original.name}
|
||||
hostPlatform={cellProps.row.original.platform}
|
||||
profileUUID={cellProps.row.original.profile_uuid}
|
||||
profile={cellProps.row.original}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Details",
|
||||
Header: <span className="sr-only">Actions</span>,
|
||||
disableSortBy: true,
|
||||
accessor: "detail",
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
|
|
@ -109,7 +110,7 @@ const generateTableConfig = (
|
|||
REC_LOCK_SYNTHETIC_PROFILE_UUID;
|
||||
|
||||
return (
|
||||
<OSSettingsErrorCell
|
||||
<OSSettingsResendCell
|
||||
canResendProfiles={
|
||||
canResendProfiles &&
|
||||
(isWindowsProfile ||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,21 @@
|
|||
&__wrapper {
|
||||
width: initial;
|
||||
}
|
||||
tbody td.detail__cell {
|
||||
// these styles are a little trick that allows the cell to
|
||||
// shrink while still follwing the auto width behavior of the table.
|
||||
// This is needed to show the error text and button in the
|
||||
// error cell correctly without overflowing outside of the table.
|
||||
max-width: 0;
|
||||
width: 100%;
|
||||
&__table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
tbody td {
|
||||
.os-settings-name-cell {
|
||||
max-width: 166px;
|
||||
}
|
||||
.os-settings-error-cell {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
th.detail__header {
|
||||
border-left: none;
|
||||
width: 120px;
|
||||
}
|
||||
th.status__header {
|
||||
width: 200px;
|
||||
}
|
||||
// Name column absorbs remaining space and truncates with ellipsis.
|
||||
tbody td.name__cell {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,25 +32,25 @@
|
|||
|
||||
// row hover effect for resend button. we dont want this behavior when the
|
||||
// button is resending
|
||||
.resend-link:not(.os-settings-error-cell__resending) {
|
||||
.resend-link:not(.os-settings-resend-cell__resending) {
|
||||
opacity: 0;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.resend-link:not(.os-settings-error-cell__resending) {
|
||||
.resend-link:not(.os-settings-resend-cell__resending) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// row hover effect for rotate button, matching resend pattern
|
||||
.rotate-link:not(.os-settings-error-cell__rotating) {
|
||||
.rotate-link:not(.os-settings-resend-cell__rotating) {
|
||||
opacity: 0;
|
||||
transition: opacity 250ms;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
.rotate-link:not(.os-settings-error-cell__rotating) {
|
||||
.rotate-link:not(.os-settings-resend-cell__rotating) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue