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:
Magnus Jensen
2026-06-17 17:23:53 +02:00
committed by GitHub
parent fe4d74edc2
commit aa24188283
11 changed files with 130 additions and 73 deletions
@@ -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", () => {
+2 -1
View File
@@ -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();
@@ -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}
@@ -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}
/>
);
};
@@ -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} />
),
@@ -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.
@@ -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,
});
});
});
@@ -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", "Couldnt 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
@@ -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;