Feat UI update macos windows setup (#12744)

relates to #12168

Updates the fleet UI so that the macOS mdm setup flow is similar to the
windows mdm setup. This includes:

**Adding a new macos setup card:**


![image](https://github.com/fleetdm/fleet/assets/1153709/dc3371a6-864b-4af8-8e97-d716d5a51361)


![image](https://github.com/fleetdm/fleet/assets/1153709/5d7ebb54-6c80-45ac-86b2-93d452357b96)

**Adding a new mac os mdm page on a new URL:**


![image](https://github.com/fleetdm/fleet/assets/1153709/30e42176-d4ab-4087-bde0-d74c81fde613)

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Gabriel Hernandez 2023-07-17 15:51:40 +01:00 committed by GitHub
parent e919f32e68
commit c5a4fa60b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 512 additions and 139 deletions

View file

@ -0,0 +1 @@
- update macos mdm setup UI in fleet UI

View file

@ -0,0 +1,16 @@
import { IMdmApple } from "interfaces/mdm";
const DEFAULT_MDM_APPLE_MOCK: IMdmApple = {
common_name: "APSP:12345",
serial_number: "12345",
issuer: "Test Certification Authority",
renew_date: "2023-03-24T22:13:59Z",
};
export const createMockMdmApple = (
overrides?: Partial<IMdmApple>
): IMdmApple => {
return { ...DEFAULT_MDM_APPLE_MOCK, ...overrides };
};
export default createMockMdmApple;

View file

@ -0,0 +1,14 @@
import { AxiosError } from "axios";
const DEFAULT_AXIOS_ERROR_MOCK: AxiosError = {
isAxiosError: true,
toJSON: () => ({}),
name: "Error",
message: "error message",
};
const createMockAxiosError = (overrides?: Partial<AxiosError>): AxiosError => {
return { ...DEFAULT_AXIOS_ERROR_MOCK, ...overrides };
};
export default createMockAxiosError;

View file

@ -18,7 +18,7 @@ const mockRouter = {
};
describe("Integrations Page", () => {
it("renders the MDM section in the side nav if MDM feature is enabled", () => {
it("renders the MDM sidenav and content if MDM feature is enabled", () => {
const render = createCustomRenderer({
withBackendMock: true,
context: {
@ -30,8 +30,8 @@ describe("Integrations Page", () => {
<IntegrationsPage router={mockRouter} params={{ section: "mdm" }} />
);
expect(
screen.getByText("Mobile device management (MDM)")
).toBeInTheDocument();
expect(screen.getAllByText("Mobile device management (MDM)")).toHaveLength(
2
);
});
});

View file

@ -0,0 +1,171 @@
import React, { useState } from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import PATHS from "router/paths";
import mdmAppleAPI from "services/entities/mdm_apple";
import { IMdmApple } from "interfaces/mdm";
import { readableDate } from "utilities/helpers";
import BackLink from "components/BackLink";
import MainContent from "components/MainContent";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import RequestCSRModal from "../components/RequestCSRModal";
const baseClass = "mac-os-mdm-page";
interface IApplePuushCertificatePortalSetupProps {
onClickRequest: () => void;
}
const ApplePushCertificatePortalSetup = ({
onClickRequest,
}: IApplePuushCertificatePortalSetupProps) => {
return (
<div className={`${baseClass}__page-content ${baseClass}__setup-content`}>
<p className={`${baseClass}__setup-description`}>
Connect Fleet to Apple Push Certificates Portal to change settings and
install software on your macOS hosts.
</p>
<ol className={`${baseClass}__setup-instructions-list`}>
<li>
<p>
1. Request a certificate signing request (CSR) and key for Apple
Push Notification Service (APNs) and a certificate and key for
Simple Certificate Enrollment Protocol (SCEP).
</p>
<Button
className={`${baseClass}__request-button`}
onClick={onClickRequest}
variant="brand"
>
Request
</Button>
</li>
<li>
<p>2. Go to your email to download your CSR.</p>
</li>
<li>
<p>
3.{" "}
<CustomLink
url="https://identity.apple.com/pushcert/"
text="Sign in to Apple Push Certificates Portal"
newTab
/>
<br />
If you don&apos;t have an Apple ID, select <b>Create yours now</b>.
</p>
</li>
<li>
<p>
4. In Apple Push Certificates Portal, select{" "}
<b>Create a Certificate</b>, upload your CSR, and download your APNs
certificate.
</p>
</li>
<li>
<p>
5. Deploy Fleet with <b>mdm</b> configuration.{" "}
<CustomLink
url="https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm"
text="See how"
newTab
/>
</p>
</li>
</ol>
</div>
);
};
interface IApplePushCertificatePortalSetupInfoProps {
appleAPNInfo: IMdmApple;
}
const ApplePushCertificatePortalSetupInfo = ({
appleAPNInfo,
}: IApplePushCertificatePortalSetupInfoProps) => {
return (
<dl className={`${baseClass}__page-content ${baseClass}__apc-info`}>
<div>
<dt>Common name (CN)</dt>
<dd>{appleAPNInfo.common_name}</dd>
</div>
<div>
<dt>Serial number</dt>
<dd>{appleAPNInfo.serial_number}</dd>
</div>
<div>
<dt>Issuer</dt>
<dd>{appleAPNInfo.issuer}</dd>
</div>
<div>
<dt>Renew date</dt>
<dd>{readableDate(appleAPNInfo.renew_date)}</dd>
</div>
</dl>
);
};
const MacOSMdmPage = () => {
const [showRequestCSRModal, setShowRequestCSRModal] = useState(false);
// Currently the status of this API call is what determines various UI states on
// this page. Because of this we will not render any of this components UI until this API
// call has completed.
const {
data: appleAPNInfo,
isLoading: isLoadingMdmApple,
error: errorMdmApple,
} = useQuery<IMdmApple, AxiosError, IMdmApple>(["appleAPNInfo"], () =>
mdmAppleAPI.getAppleAPNInfo()
);
const toggleRequestCSRModal = () => {
setShowRequestCSRModal((prevState) => !prevState);
};
const renderPageContent = () => {
// The API returns a 404 error if APNs is not configured yet, in that case we
// want to prompt the user to download the certs and keys to configure the
// server instead of the default error message.
const showMdmAppleError = errorMdmApple && errorMdmApple.status !== 404;
if (showMdmAppleError) {
return <DataError />;
}
if (!appleAPNInfo) {
return (
<ApplePushCertificatePortalSetup
onClickRequest={toggleRequestCSRModal}
/>
);
}
return <ApplePushCertificatePortalSetupInfo appleAPNInfo={appleAPNInfo} />;
};
return (
<MainContent className={baseClass}>
<>
<BackLink
text="Back to MDM"
path={PATHS.ADMIN_INTEGRATIONS_MDM}
className={`${baseClass}__back-to-mdm`}
/>
<h1>Apple Push Certificate Portal</h1>
{isLoadingMdmApple ? <Spinner /> : renderPageContent()}
{showRequestCSRModal && (
<RequestCSRModal onCancel={toggleRequestCSRModal} />
)}
</>
</MainContent>
);
};
export default MacOSMdmPage;

View file

@ -0,0 +1,58 @@
.mac-os-mdm-page {
&__back-to-mdm {
margin-bottom: $pad-xlarge;
}
h1 {
margin-bottom: $pad-xxlarge;
font-size: $x-large;
}
h4 {
margin-bottom: 0;
}
p {
font-size: $x-small;
margin: 0 0 $pad-large;
}
&__page-content {
font-size: $x-small;
}
&__setup-content {
display: flex;
flex-direction: column;
gap: $pad-large;
color: $core-fleet-black;
p {
margin: 0;
}
}
&__setup-instructions-list {
padding: 0;
margin: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $pad-large;
};
&__request-button {
margin-top: $pad-small;
}
&__apc-info {
display: flex;
flex-direction: column;
gap: $pad-medium;
dt {
font-weight: $bold;
margin-bottom: $pad-xsmall;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./MacOSMdmPage";

View file

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react";
import React, { useContext } from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { InjectedRouter } from "react-router";
@ -8,16 +8,12 @@ import { AppContext } from "context/app";
import mdmAppleAPI from "services/entities/mdm_apple";
import { IMdmApple } from "interfaces/mdm";
import { readableDate } from "utilities/helpers";
import PATHS from "router/paths";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
import Spinner from "components/Spinner";
import DataError from "components/DataError";
import RequestCSRModal from "./components/RequestCSRModal";
import EndUserMigrationSection from "./components/EndUserMigrationSection/EndUserMigrationSection";
import WindowsMdmSection from "./components/WindowsMdmSection/WindowsMdmSection";
import WindowsMdmCard from "./components/WindowsMdmCard/WindowsMdmCard";
import MacOSMdmCard from "./components/MacOSMdmCard/MacOSMdmCard";
const baseClass = "mdm-settings";
@ -28,8 +24,9 @@ interface IMdmSettingsProps {
const MdmSettings = ({ router }: IMdmSettingsProps) => {
const { isPremiumTier, config } = useContext(AppContext);
const [showRequestCSRModal, setShowRequestCSRModal] = useState(false);
// Currently the status of this API call is what determines various UI states on
// this page. Because of this we will not render any of this components UI until this API
// call has completed.
const {
data: appleAPNInfo,
isLoading: isLoadingMdmApple,
@ -44,111 +41,42 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
}
);
const toggleRequestCSRModal = () => {
setShowRequestCSRModal(!showRequestCSRModal);
const navigateToMacOSMdm = () => {
router.push(PATHS.ADMIN_INTEGRATIONS_MDM_MAC);
};
const navigateToWindowsMdm = () => {
router.push(PATHS.ADMIN_INTEGRATIONS_MDM_WINDOWS);
};
// The API returns a 404 error if APNs is not configured yet, in that case we
// want to prompt the user to download the certs and keys to configure the
// server instead of the default error message.
const showMdmAppleError = errorMdmApple && errorMdmApple.status !== 404;
const renderMdmAppleSection = () => {
if (showMdmAppleError) {
return <DataError />;
}
if (!appleAPNInfo) {
return (
<>
<div className={`${baseClass}__section-description`}>
Connect Fleet to Apple Push Certificates Portal to change settings
and install software on your macOS hosts.
</div>
<div className={`${baseClass}__section-instructions`}>
<p>
1. Request a certificate signing request (CSR) and key for Apple
Push Notification Service (APNs) and a certificate and key for
Simple Certificate Enrollment Protocol (SCEP).
</p>
<Button onClick={toggleRequestCSRModal} variant="brand">
Request
</Button>
<p>2. Go to your email to download your CSR.</p>
<p>
3.{" "}
<CustomLink
url="https://identity.apple.com/pushcert/"
text="Sign in to Apple Push Certificates Portal"
newTab
/>
<br />
If you dont have an Apple ID, select <b>Create yours now</b>.
</p>
<p>
4. In Apple Push Certificates Portal, select{" "}
<b>Create a Certificate</b>, upload your CSR, and download your
APNs certificate.
</p>
<p>
5. Deploy Fleet with <b>mdm</b> configuration.{" "}
<CustomLink
url="https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm"
text="See how"
newTab
/>
</p>
</div>
</>
);
}
return (
<>
<div className={`${baseClass}__section-description`}>
To change settings and install software on your macOS hosts, Apple
Inc. requires an Apple Push Notification service (APNs) certificate.
</div>
<div className={`${baseClass}__section-information`}>
<h4>Common name (CN)</h4>
<p>{appleAPNInfo.common_name}</p>
<h4>Serial number</h4>
<p>{appleAPNInfo.serial_number}</p>
<h4>Issuer</h4>
<p>{appleAPNInfo.issuer}</p>
<h4>Renew date</h4>
<p>{readableDate(appleAPNInfo.renew_date)}</p>
</div>
</>
);
};
return (
<div className={baseClass}>
<div className={`${baseClass}__section`}>
<h2>Apple Push Certificates Portal</h2>
{isLoadingMdmApple ? <Spinner /> : renderMdmAppleSection()}
<div className={`${baseClass}__section ${baseClass}__mdm-section`}>
<h2>Mobile device management (MDM)</h2>
{isLoadingMdmApple ? (
<Spinner />
) : (
<>
<MacOSMdmCard
appleAPNInfo={appleAPNInfo}
errorData={errorMdmApple}
turnOnMacOSMdm={navigateToMacOSMdm}
viewDetails={navigateToMacOSMdm}
/>
{/* TODO: remove conditional rendering when windows MDM is released. */}
{config?.mdm_enabled && (
<WindowsMdmCard
turnOnWindowsMdm={navigateToWindowsMdm}
editWindowsMdm={navigateToWindowsMdm}
/>
)}
</>
)}
</div>
{/* TODO: remove conditional rendering when windows MDM is released. */}
{config?.mdm_enabled && (
<WindowsMdmSection
turnOnWindowsMdm={navigateToWindowsMdm}
editWindowsMdm={navigateToWindowsMdm}
/>
)}
{isPremiumTier && (
<>
<div className={`${baseClass}__section`}>
<EndUserMigrationSection router={router} />
</div>
</>
)}
{showRequestCSRModal && (
<RequestCSRModal onCancel={toggleRequestCSRModal} />
{isPremiumTier && appleAPNInfo && (
<div className={`${baseClass}__section`}>
<EndUserMigrationSection router={router} />
</div>
)}
</div>
);

View file

@ -6,6 +6,7 @@
h1 {
margin-bottom: $pad-xxlarge;
font-size: $x-large;
}
p {

View file

@ -5,20 +5,15 @@
gap: 80px;
&__section {
margin: 0 0 $pad-large;
h2 {
margin-bottom: 0;
padding-bottom: $pad-small;
max-width: 100%;
font-size: $medium;
font-weight: $regular;
color: $core-fleet-black;
border-bottom: solid 1px $ui-fleet-black-10;
margin: 0 0 $pad-xxlarge;
}
h4 {
margin-bottom: 0;
}
.mdm-settings-team-btn {
@ -34,17 +29,9 @@
}
}
&__section-description,
&__section-instructions,
&__section-information {
font-size: $x-small;
color: $core-fleet-black;
width: 100%;
}
&__section-information {
p {
margin: 0;
}
&__mdm-section {
display: flex;
flex-direction: column;
gap: 40px;
}
}

View file

@ -139,8 +139,8 @@ const EndUserMigrationSection = ({ router }: IEndUserMigrationSectionProps) => {
<div className={baseClass}>
<h2>End user migration workflow</h2>
<p>
Control the end user migration workflow for hosts that automatically
enrolled to your old MDM solution.
Control the end user migration workflow for macOS hosts that
automatically enrolled to your old MDM solution.
</p>
<img

View file

@ -0,0 +1,58 @@
import React from "react";
import { noop } from "lodash";
import { render, screen } from "@testing-library/react";
import createMockMdmApple from "__mocks__/appleMdm";
import createMockAxiosError from "__mocks__/axiosError";
import MacOSMdmCard from "./MacOSMdmCard";
describe("MacOSMdmCard", () => {
it("renders the turn on macOs mdm state when there is no appleAPNInfo", () => {
render(
<MacOSMdmCard
appleAPNInfo={undefined}
errorData={null}
turnOnMacOSMdm={noop}
viewDetails={noop}
/>
);
expect(screen.getByText("Turn on macOS MDM")).toBeInTheDocument();
});
it("renders the show details state when there is appleAPNInfo", () => {
render(
<MacOSMdmCard
appleAPNInfo={createMockMdmApple()}
errorData={null}
turnOnMacOSMdm={noop}
viewDetails={noop}
/>
);
expect(screen.getByText("macOS MDM turned on")).toBeInTheDocument();
});
it("renders the error state when there is a non 404 error", () => {
render(
<MacOSMdmCard
appleAPNInfo={createMockMdmApple()}
errorData={createMockAxiosError({ status: 500 })}
turnOnMacOSMdm={noop}
viewDetails={noop}
/>
);
expect(screen.getByText(/Something's gone wrong/)).toBeInTheDocument();
render(
<MacOSMdmCard
appleAPNInfo={createMockMdmApple()}
errorData={createMockAxiosError({ status: 404 })}
turnOnMacOSMdm={noop}
viewDetails={noop}
/>
);
});
});

View file

@ -0,0 +1,88 @@
import React from "react";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import Card from "components/Card";
import DataError from "components/DataError";
import { AxiosError } from "axios";
import { IMdmApple } from "interfaces/mdm";
const baseClass = "mac-os-mdm-card";
interface ITurnOnMacOSMdmProps {
onClickTurnOn: () => void;
}
const TurnOnMacOSMdm = ({ onClickTurnOn }: ITurnOnMacOSMdmProps) => {
return (
<div className={`${baseClass}__turn-on-mac-os`}>
<div>
<h3>Turn on macOS MDM</h3>
<p>
Connect Fleet to Apple Push Certificates Portal to change settings and
install software on your macOS hosts.
</p>
</div>
<Button onClick={onClickTurnOn}>Connect APNS</Button>
</div>
);
};
interface ITurnOffMacOSMdmProps {
onClickDetails: () => void;
}
const SeeDetailsMacOSMdm = ({ onClickDetails }: ITurnOffMacOSMdmProps) => {
return (
<div className={`${baseClass}__turn-off-mac-os`}>
<div>
<Icon name="success" />
<p>macOS MDM turned on</p>
</div>
<Button onClick={onClickDetails} variant="text-icon">
Details
<Icon name="chevron" direction="right" color="core-fleet-blue" />
</Button>
</div>
);
};
interface IMacOSMdmCardProps {
appleAPNInfo: IMdmApple | undefined;
errorData: AxiosError | null;
turnOnMacOSMdm: () => void;
viewDetails: () => void;
}
/**
* This compoent is responsible for showing the correct UI for the macOS MDM card.
* We pass in the appleAPNInfo and errorData from the MdmSettings component because
* we need to make that API call higher up in the component tree to correctly show
* loading states on the page.
*/
const MacOSMdmCard = ({
appleAPNInfo,
errorData,
turnOnMacOSMdm,
viewDetails,
}: IMacOSMdmCardProps) => {
// The API returns a 404 error if APNS is not configured yet. If there is any
// other error we will show the DataError component.
const showError = errorData !== null && errorData.status !== 404;
if (showError) {
return <DataError />;
}
return (
<Card className={baseClass} color="gray">
{appleAPNInfo !== undefined ? (
<SeeDetailsMacOSMdm onClickDetails={viewDetails} />
) : (
<TurnOnMacOSMdm onClickTurnOn={turnOnMacOSMdm} />
)}
</Card>
);
};
export default MacOSMdmCard;

View file

@ -0,0 +1,37 @@
.mac-os-mdm-card {
font-size: $x-small;
p {
margin: 0;
}
&__turn-on-mac-os,
&__turn-off-mac-os {
display: flex;
justify-content: space-between;
align-items: center;
}
&__turn-on-mac-os {
h3 {
font-size: $x-small;
font-weight: $bold;
margin: 0 0 $pad-xsmall;
}
p {
max-width: 520px;
}
}
&__turn-off-mac-os {
>div {
display: flex;
align-items: center;
}
p {
margin-left: $pad-small;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./MacOSMdmCard";

View file

@ -6,15 +6,16 @@ import Card from "components/Card/Card";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
const baseClass = "windows-mdm-section";
const baseClass = "windows-mdm-card";
interface ITurnOnWindowsMdmProps {
onClickTurnOn: () => void;
}
const TurnOnWindowsMdm = ({ onClickTurnOn }: ITurnOnWindowsMdmProps) => {
return (
<div className={`${baseClass}__turn-on-windows`}>
<div className={`${baseClass}__`}>
<div>
<h3>Turn on Windows MDM</h3>
<p>Turn MDM on for Windows hosts with fleetd.</p>
</div>
@ -42,15 +43,15 @@ const TurnOffWindowsMdm = ({ onClickEdit }: ITurnOffWindowsMdmProps) => {
);
};
interface IWindowsMdmSectionProps {
interface IWindowsMdmCardProps {
turnOnWindowsMdm: () => void;
editWindowsMdm: () => void;
}
const WindowsMdmSection = ({
const WindowsMdmCard = ({
turnOnWindowsMdm,
editWindowsMdm,
}: IWindowsMdmSectionProps) => {
}: IWindowsMdmCardProps) => {
const { config } = useContext(AppContext);
const isWindowsMdmEnabled =
@ -67,4 +68,4 @@ const WindowsMdmSection = ({
);
};
export default WindowsMdmSection;
export default WindowsMdmCard;

View file

@ -1,4 +1,4 @@
.windows-mdm-section {
.windows-mdm-card {
font-size: $x-small;
p {
@ -29,5 +29,4 @@
margin-left: $pad-small;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./WindowsMdmCard";

View file

@ -1 +0,0 @@
export { default } from "./WindowsMdmSection";

View file

@ -53,7 +53,8 @@ import AgentOptionsPage from "pages/admin/TeamManagementPage/TeamDetailsWrapper/
import MacOSUpdates from "pages/ManageControlsPage/MacOSUpdates";
import MacOSSettings from "pages/ManageControlsPage/MacOSSettings";
import MacOSSetup from "pages/ManageControlsPage/MacOSSetup/MacOSSetup";
import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage/WindowsMdmPage";
import WindowsMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/WindowsMdmPage";
import MacOSMdmPage from "pages/admin/IntegrationsPage/cards/MdmSettings/MacOSMdmPage";
import PATHS from "router/paths";
@ -138,6 +139,7 @@ const routes = (
</Route>
</Route>
<Route path="integrations/mdm/windows" component={WindowsMdmPage} />
<Route path="integrations/mdm/apple" component={MacOSMdmPage} />
<Route path="teams" component={TeamDetailsWrapper}>
<Route path="members" component={MembersPage} />
<Route path="options" component={AgentOptionsPage} />

View file

@ -22,6 +22,7 @@ export default {
ADMIN_INTEGRATIONS: `${URL_PREFIX}/settings/integrations`,
ADMIN_INTEGRATIONS_TICKET_DESTINATIONS: `${URL_PREFIX}/settings/integrations/ticket-destinations`,
ADMIN_INTEGRATIONS_MDM: `${URL_PREFIX}/settings/integrations/mdm`,
ADMIN_INTEGRATIONS_MDM_MAC: `${URL_PREFIX}/settings/integrations/mdm/apple`,
ADMIN_INTEGRATIONS_MDM_WINDOWS: `${URL_PREFIX}/settings/integrations/mdm/windows`,
ADMIN_INTEGRATIONS_AUTOMATIC_ENROLLMENT: `${URL_PREFIX}/settings/integrations/automatic-enrollment`,
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,

View file

@ -136,3 +136,12 @@ hr {
border: none;
border-bottom: 1px solid $ui-fleet-black-10;
}
dl {
margin: 0;
padding: 0;
}
dd {
margin: 0;
}