BYOF: Frontend changes (#47523)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45601 Responsiveness: https://github.com/user-attachments/assets/d7ea6297-8677-4093-b343-b76c64121f53 Removed border from Action header: <img width="1450" height="134" alt="image" src="https://github.com/user-attachments/assets/1dad670c-9f11-4911-862f-2d77a2e5c1cf" /> # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. In another PR - [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 ## Summary by CodeRabbit * **New Features** * Added client-side search by organization name in the Apple Business Manager table. * Enabled **BYOD fleet** selection in Apple Business Manager fleet editing and saving. * **Bug Fixes** * Standardized enrollment status text to consistently show **“On (BYOD)”** for BYOD/personal cases. * **Improvements** * Enhanced ABM table sorting and responsive column visibility. * Updated Apple Business Manager modal layout, wording, and success messaging. * Prevented renew-date tooltips from relying on missing configuration. * **Tests** * Updated MDM/BYOD status and fleet modal tests for the new labels and BYOD handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+3
-3
@@ -33,9 +33,9 @@ describe("HostMdmStatusCell", () => {
|
||||
expect(screen.getByText("On (company-owned)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders 'On (personal)' for iOS hosts with personal enrollment", () => {
|
||||
it("renders 'On (BYOD)' for iOS hosts with personal enrollment", () => {
|
||||
renderCell("ios", "On (personal)");
|
||||
expect(screen.getByText("On (personal)")).toBeInTheDocument();
|
||||
expect(screen.getByText("On (BYOD)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders 'Pending' for macOS hosts with pending enrollment", () => {
|
||||
@@ -45,7 +45,7 @@ describe("HostMdmStatusCell", () => {
|
||||
|
||||
it("renders the MDM status for Android hosts", () => {
|
||||
renderCell("android", "On (personal)");
|
||||
expect(screen.getByText("On (personal)")).toBeInTheDocument();
|
||||
expect(screen.getByText("On (BYOD)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the MDM status for Windows hosts", () => {
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface IMdmAbToken {
|
||||
macos_fleet: ITokenFleet;
|
||||
ios_fleet: ITokenFleet;
|
||||
ipados_fleet: ITokenFleet;
|
||||
byod_fleet: ITokenFleet;
|
||||
}
|
||||
|
||||
export interface IMdmVppToken {
|
||||
@@ -96,7 +97,7 @@ export const MDM_ENROLLMENT_STATUS_UI_MAP: Record<
|
||||
filterValue: "automatic",
|
||||
},
|
||||
"On (personal)": {
|
||||
displayName: "On (personal)",
|
||||
displayName: "On (BYOD)",
|
||||
filterValue: "personal",
|
||||
},
|
||||
Off: {
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("MDM Card", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("row", {
|
||||
name: /On \(personal\)(.*?)3 view all hosts/i,
|
||||
name: /On \(BYOD\)(.*?)3 view all hosts/i,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
|
||||
|
||||
+1
-2
@@ -184,8 +184,7 @@ const AppleBusinessManagerPage = ({ router }: { router: InjectedRouter }) => {
|
||||
<>
|
||||
<p>
|
||||
Add your AB to enable automatic enrollment for company-owned hosts
|
||||
and enrollment, via a Managed Apple Account, for personal (BYOD)
|
||||
hosts.
|
||||
and enrollment, via a Managed Apple Account, for BYOD hosts.
|
||||
</p>
|
||||
<AppleBusinessManagerTable
|
||||
abTokens={abTokens}
|
||||
|
||||
+18
-3
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { IMdmAbToken } from "interfaces/mdm";
|
||||
import useGitOpsMode from "hooks/useGitOpsMode";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
|
||||
import { generateTableConfig } from "./AppleBusinessManagerTableConfig";
|
||||
|
||||
@@ -23,6 +24,13 @@ const AppleBusinessManagerTable = ({
|
||||
onDeleteToken,
|
||||
}: IAppleBusinessManagerTableProps) => {
|
||||
const { gitOpsModeEnabled, repoURL } = useGitOpsMode();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const normalizedQuery = searchQuery.toLowerCase();
|
||||
const filteredAbTokens = normalizedQuery
|
||||
? abTokens.filter((token) =>
|
||||
token.org_name.toLowerCase().includes(normalizedQuery)
|
||||
)
|
||||
: abTokens;
|
||||
|
||||
const onSelectAction = (action: string, abmToken: IMdmAbToken) => {
|
||||
switch (action) {
|
||||
@@ -46,18 +54,25 @@ const AppleBusinessManagerTable = ({
|
||||
repoURL
|
||||
);
|
||||
|
||||
const onQueryChange = (queryData: ITableQueryData) => {
|
||||
setSearchQuery(queryData.searchQuery);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer<IMdmAbToken>
|
||||
columnConfigs={tableConfig}
|
||||
defaultSortHeader="org_name"
|
||||
disableTableHeader
|
||||
disablePagination
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
emptyComponent={() => <></>}
|
||||
isLoading={false}
|
||||
data={abTokens}
|
||||
data={filteredAbTokens}
|
||||
className={baseClass}
|
||||
searchable
|
||||
inputPlaceHolder="Search by organization name"
|
||||
searchQuery={searchQuery}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+59
-19
@@ -92,8 +92,13 @@ export const generateTableConfig = (
|
||||
},
|
||||
{
|
||||
accessor: "renew_date",
|
||||
Header: "Renew date",
|
||||
disableSortBy: true,
|
||||
sortType: "dateStrings",
|
||||
Header: (cellProps: ITableHeaderProps) => (
|
||||
<HeaderCell
|
||||
value="Renew date"
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
),
|
||||
Cell: (cellProps: IRenewDateCellProps) => (
|
||||
<RenewDateCell
|
||||
value={cellProps.cell.value}
|
||||
@@ -102,18 +107,11 @@ export const generateTableConfig = (
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "apple_id",
|
||||
Header: "Apple ID",
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "macos_team",
|
||||
sortType: "caseInsensitive",
|
||||
accessor: (originalRow) => getFleetDisplayName(originalRow.macos_fleet),
|
||||
Header: () => {
|
||||
Header: (cellProps: ITableHeaderProps) => {
|
||||
const titleWithToolTip = (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
@@ -128,17 +126,22 @@ export const generateTableConfig = (
|
||||
macOS fleet
|
||||
</TooltipWrapper>
|
||||
);
|
||||
return <HeaderCell value={titleWithToolTip} disableSortBy />;
|
||||
return (
|
||||
<HeaderCell
|
||||
value={titleWithToolTip}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ios_team",
|
||||
sortType: "caseInsensitive",
|
||||
accessor: (originalRow) => getFleetDisplayName(originalRow.ios_fleet),
|
||||
Header: () => {
|
||||
Header: (cellProps: ITableHeaderProps) => {
|
||||
const titleWithToolTip = (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
@@ -153,17 +156,22 @@ export const generateTableConfig = (
|
||||
iOS fleet
|
||||
</TooltipWrapper>
|
||||
);
|
||||
return <HeaderCell value={titleWithToolTip} disableSortBy />;
|
||||
return (
|
||||
<HeaderCell
|
||||
value={titleWithToolTip}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ipados_team",
|
||||
sortType: "caseInsensitive",
|
||||
accessor: (originalRow) => getFleetDisplayName(originalRow.ipados_fleet),
|
||||
Header: () => {
|
||||
Header: (cellProps: ITableHeaderProps) => {
|
||||
const titleWithToolTip = (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
@@ -178,9 +186,41 @@ export const generateTableConfig = (
|
||||
iPadOS fleet
|
||||
</TooltipWrapper>
|
||||
);
|
||||
return <HeaderCell value={titleWithToolTip} disableSortBy />;
|
||||
return (
|
||||
<HeaderCell
|
||||
value={titleWithToolTip}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "byod_team",
|
||||
sortType: "caseInsensitive",
|
||||
accessor: (originalRow) => getFleetDisplayName(originalRow.byod_fleet),
|
||||
Header: (cellProps: ITableHeaderProps) => {
|
||||
const titleWithToolTip = (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
iOS/iPadOS hosts that enroll via Managed Apple Account are
|
||||
automatically added to this fleet.
|
||||
</>
|
||||
}
|
||||
>
|
||||
BYOD fleet
|
||||
</TooltipWrapper>
|
||||
);
|
||||
return (
|
||||
<HeaderCell
|
||||
value={titleWithToolTip}
|
||||
isSortedDesc={cellProps.column.isSortedDesc}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps: ITableStringCellProps) => (
|
||||
<TextCell value={cellProps.cell.value} />
|
||||
),
|
||||
|
||||
+14
-13
@@ -1,12 +1,9 @@
|
||||
.apple-business-manager-table {
|
||||
.data-table-block .data-table {
|
||||
td.apple_id__cell {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
td.macos_team__cell,
|
||||
td.ios_team__cell,
|
||||
td.ipados_team__cell {
|
||||
td.ipados_team__cell,
|
||||
td.byod_team__cell {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
@@ -16,6 +13,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
th.id__header {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// The desired behavior is to hide the header and team cell one by one
|
||||
@@ -23,26 +24,26 @@
|
||||
// media query with the breakpoint values taken from when the table content
|
||||
// starts to overflow.
|
||||
@media (max-width: $break-lg) {
|
||||
.byod_team__header,
|
||||
.byod_team__cell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1230px) {
|
||||
.ipados_team__header,
|
||||
.ipados_team__cell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1230px) {
|
||||
@media (max-width: $break-md) {
|
||||
.ios_team__header,
|
||||
.ios_team__cell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
.macos_team__header,
|
||||
.macos_team__cell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// this is a special case where the org name cell is too long at smaller widths.
|
||||
// We dont want to remove any more columns so we need to reduce the width of
|
||||
// the org name cell content to prevent table content overflow.
|
||||
|
||||
+3
@@ -10,6 +10,7 @@ describe("EditTeamsAbmModal", () => {
|
||||
APP_CONTEXT_NO_TEAM_SUMMARY,
|
||||
{ name: "Team 1", id: 1 },
|
||||
{ name: "Team 2", id: 2 },
|
||||
{ name: "BYOD Fleet", id: 3 },
|
||||
];
|
||||
|
||||
describe("getOptions", () => {
|
||||
@@ -30,11 +31,13 @@ describe("EditTeamsAbmModal", () => {
|
||||
ios_team: "Team 1",
|
||||
ipados_team: "Team 2",
|
||||
macos_team: "Unassigned",
|
||||
byod_team: "BYOD Fleet",
|
||||
};
|
||||
expect(getSelectedTeamIds(selectedTeamNames, availableTeams)).toEqual({
|
||||
ios_fleet_id: 1,
|
||||
ipados_fleet_id: 2,
|
||||
macos_fleet_id: 0,
|
||||
byod_fleet_id: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+26
-29
@@ -12,6 +12,8 @@ import Modal from "components/Modal";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Button from "components/buttons/Button";
|
||||
import FormField from "components/forms/FormField";
|
||||
import RenewDateCell from "../../../components/RenewDateCell";
|
||||
|
||||
const baseClass = "edit-teams-abm-modal";
|
||||
|
||||
@@ -41,6 +43,7 @@ interface SelectedTeamNames {
|
||||
ios_team: IMdmAbToken["ios_fleet"]["name"];
|
||||
ipados_team: IMdmAbToken["ipados_fleet"]["name"];
|
||||
macos_team: IMdmAbToken["macos_fleet"]["name"];
|
||||
byod_team: IMdmAbToken["byod_fleet"]["name"];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +59,7 @@ type SelectedTeamIds = Parameters<typeof mdmAbmAPI.editTeams>[0]["teams"];
|
||||
* `validateSelectedTeamIds` function).
|
||||
*/
|
||||
export const getSelectedTeamIds = (
|
||||
{ ios_team, ipados_team, macos_team }: SelectedTeamNames,
|
||||
{ ios_team, ipados_team, macos_team, byod_team }: SelectedTeamNames,
|
||||
availableTeams: ITeamSummary[] = []
|
||||
): SelectedTeamIds => {
|
||||
const byName = availableTeams.reduce((acc, t) => {
|
||||
@@ -67,6 +70,7 @@ export const getSelectedTeamIds = (
|
||||
ios_fleet_id: byName[ios_team],
|
||||
ipados_fleet_id: byName[ipados_team],
|
||||
macos_fleet_id: byName[macos_team],
|
||||
byod_fleet_id: byName[byod_team],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -85,6 +89,7 @@ const EditTeamsAbmModal = ({
|
||||
ios_team: token.ios_fleet.name,
|
||||
ipados_team: token.ipados_fleet.name,
|
||||
macos_team: token.macos_fleet.name,
|
||||
byod_team: token.byod_fleet.name,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -107,7 +112,7 @@ const EditTeamsAbmModal = ({
|
||||
tokenId: token.id,
|
||||
teams: getSelectedTeamIds(selectedTeamNames, availableTeams),
|
||||
});
|
||||
renderFlash("success", "Edited successfully.");
|
||||
renderFlash("success", "Successfully updated fleets for AB token.");
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn’t edit. Please try again.");
|
||||
@@ -127,15 +132,21 @@ const EditTeamsAbmModal = ({
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
title="Edit fleets"
|
||||
title={token.org_name}
|
||||
onExit={onCancel}
|
||||
width="large"
|
||||
isContentDisabled={isSaving}
|
||||
>
|
||||
<p>
|
||||
Edit fleets for <b>{token.org_name}</b>.
|
||||
</p>
|
||||
<form onSubmit={onSave} className={baseClass} autoComplete="off">
|
||||
<FormField name="apple_id" label="Apple ID">
|
||||
<p>{token.apple_id}</p>
|
||||
</FormField>
|
||||
<FormField name="renew_date" label="Renew date">
|
||||
<RenewDateCell
|
||||
value={token.renew_date}
|
||||
className="abm-renew-date-cell"
|
||||
/>
|
||||
</FormField>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={options}
|
||||
@@ -145,14 +156,6 @@ const EditTeamsAbmModal = ({
|
||||
value={selectedTeamNames.macos_team}
|
||||
label="macOS fleet"
|
||||
wrapperClassName={`${baseClass}__form-field form-field--macos`}
|
||||
tooltip={
|
||||
<>
|
||||
macOS hosts are automatically added to this fleet on initial sync
|
||||
from ABM. If a host is manually assigned to a different fleet
|
||||
before enrollment, it will enroll to the newly assigned fleet and
|
||||
not the default.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
@@ -163,14 +166,6 @@ const EditTeamsAbmModal = ({
|
||||
value={selectedTeamNames.ios_team}
|
||||
label="iOS fleet"
|
||||
wrapperClassName={`${baseClass}__form-field form-field--ios`}
|
||||
tooltip={
|
||||
<>
|
||||
iOS hosts are automatically added to this fleet on initial sync
|
||||
from ABM. If a host is manually assigned to a different fleet
|
||||
before enrollment, it will enroll to the newly assigned fleet and
|
||||
not the default.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
@@ -181,14 +176,16 @@ const EditTeamsAbmModal = ({
|
||||
value={selectedTeamNames.ipados_team}
|
||||
label="iPadOS fleet"
|
||||
wrapperClassName={`${baseClass}__form-field form-field--ipados`}
|
||||
tooltip={
|
||||
<>
|
||||
iPadOS hosts are automatically added to this fleet on initial sync
|
||||
from ABM. If a host is manually assigned to a different fleet
|
||||
before enrollment, it will enroll to the newly assigned fleet and
|
||||
not the default.
|
||||
</>
|
||||
/>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={options}
|
||||
onChange={(value: string) =>
|
||||
setSelectedTeamNames((prev) => ({ ...prev, byod_team: value }))
|
||||
}
|
||||
value={selectedTeamNames.byod_team}
|
||||
label="BYOD fleet"
|
||||
wrapperClassName={`${baseClass}__form-field form-field--byod`}
|
||||
/>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ interface IRenewDateCellProps {
|
||||
* `statusConfig` currently this allows us to dynamically change the tooltip
|
||||
* text depending on the status of the date. Can be extended later if needed.
|
||||
*/
|
||||
statusConfig: IRenewDateCellStatusConfig;
|
||||
statusConfig?: IRenewDateCellStatusConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const RenewDateCell = ({
|
||||
indicatorStatus = "error";
|
||||
}
|
||||
|
||||
if (indicatorStatus !== "success") {
|
||||
if (indicatorStatus !== "success" && statusConfig) {
|
||||
tooltipText = statusConfig[indicatorStatus].tooltipText;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export default {
|
||||
ios_fleet_id: number;
|
||||
ipados_fleet_id: number;
|
||||
macos_fleet_id: number;
|
||||
byod_fleet_id: number;
|
||||
};
|
||||
}) => {
|
||||
const { MDM_AB_TOKEN_TEAMS } = endpoints;
|
||||
|
||||
Reference in New Issue
Block a user