Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ describe('MemberRow device status', () => {
expect(screen.getByText('Not Installed').className).toContain('text-muted-foreground');
});

it('shows "Not Installed" by default when deviceStatus is omitted', () => {
it('shows dash when deviceStatus is omitted (no compliance obligation)', () => {
renderMemberRow();
expect(screen.getByText('Not Installed')).toBeInTheDocument();
expect(screen.queryByText('Not Installed')).not.toBeInTheDocument();
expect(screen.queryByText('Compliant')).not.toBeInTheDocument();
expect(screen.queryByText('Non-Compliant')).not.toBeInTheDocument();
});

it('shows "Compliant" with green dot when deviceStatus is compliant', () => {
Expand Down Expand Up @@ -177,4 +179,58 @@ describe('MemberRow device status', () => {
const redDot = c3.querySelector('.bg-red-400');
expect(redDot).toBeInTheDocument();
});

it('does not show device status for member without compliance obligation (e.g. auditor)', () => {
const auditorMember = {
...baseMember,
role: 'auditor',
} as unknown as MemberWithUser;

render(
<table>
<tbody>
<MemberRow
member={auditorMember}
onRemove={noop}
onRemoveDevice={noop}
onUpdateRole={noop}
onReactivate={noop}
canEdit={false}
isCurrentUserOwner={false}
// deviceStatus intentionally omitted — auditor won't be in the map
/>
</tbody>
</table>,
);

expect(screen.queryByText('Not Installed')).not.toBeInTheDocument();
expect(screen.queryByText('Compliant')).not.toBeInTheDocument();
expect(screen.queryByText('Non-Compliant')).not.toBeInTheDocument();
});

it('still shows device status for member with compliance obligation', () => {
const employeeMember = {
...baseMember,
role: 'employee',
} as unknown as MemberWithUser;

render(
<table>
<tbody>
<MemberRow
member={employeeMember}
onRemove={noop}
onRemoveDevice={noop}
onUpdateRole={noop}
onReactivate={noop}
canEdit={false}
isCurrentUserOwner={false}
deviceStatus="compliant"
/>
</tbody>
</table>,
);

expect(screen.getByText('Compliant')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function MemberRow({
isCurrentUserOwner,
customRoles = [],
taskCompletion,
deviceStatus = 'not-installed',
deviceStatus,
}: MemberRowProps) {
const { orgId } = useParams<{ orgId: string }>();

Expand Down Expand Up @@ -233,7 +233,7 @@ export function MemberRow({

{/* DEVICE */}
<TableCell>
{isPlatformAdmin || isDeactivated ? (
{isPlatformAdmin || isDeactivated || !deviceStatus ? (
<Text size="sm" variant="muted">
</Text>
Expand Down Expand Up @@ -321,7 +321,7 @@ export function MemberRow({
</DropdownMenuItem>
)}
{!isDeactivated &&
(member.fleetDmLabelId || deviceStatus !== 'not-installed') &&
(member.fleetDmLabelId || (deviceStatus && deviceStatus !== 'not-installed')) &&
isCurrentUserOwner && (
<DropdownMenuItem
onSelect={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export function TeamMembersClient({
isCurrentUserOwner={isCurrentUserOwner}
customRoles={customRoles}
taskCompletion={taskCompletionMap[(item as MemberWithUser).id]}
deviceStatus={deviceStatusMap[(item as MemberWithUser).id] ?? 'not-installed'}
deviceStatus={deviceStatusMap[(item as MemberWithUser).id]}
/>
) : (
<PendingInvitationRow
Expand Down
19 changes: 15 additions & 4 deletions apps/app/src/app/(app)/[orgId]/people/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,22 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId:

// Build unified device status map from the SAME data both tabs use.
// This ensures the member list and compliance chart agree on compliance.
// Only members with compliance obligations are checked (uses RBAC obligations,
// not hardcoded role names). Members without compliance obligations (e.g.
// auditor-only, or custom roles without compliance: true) are omitted and
// will show "—" in the DEVICE column.
const deviceStatusMap: Record<string, 'compliant' | 'non-compliant' | 'not-installed'> = {};
const complianceMemberIds = new Set(employees.map((m) => m.id));

// Default all compliance members to "not-installed" — device data below overrides
for (const id of complianceMemberIds) {
deviceStatusMap[id] = 'not-installed';
}

// Device-agent devices: compliant only if ALL of a member's devices pass
const agentComplianceByMember = new Map<string, boolean>();
for (const d of agentDevices) {
if (!d.memberId) continue;
if (!d.memberId || !complianceMemberIds.has(d.memberId)) continue;
const prev = agentComplianceByMember.get(d.memberId);
agentComplianceByMember.set(d.memberId, (prev ?? true) && d.isCompliant);
}
Expand All @@ -173,12 +183,13 @@ export default async function PeoplePage({ params }: { params: Promise<{ orgId:
// Fleet-only devices: use the same merged policy data the chart uses
// (Fleet API automated checks + DB manual overrides, already combined by getFleetHosts)
for (const host of filteredFleetDevices) {
if (!host.member_id) continue;
if (!host.member_id || !complianceMemberIds.has(host.member_id)) continue;
// If already set by device-agent, skip (agent takes priority)
if (agentComplianceByMember.has(host.member_id)) continue;
const isCompliant = host.policies.every((p) => p.response === 'pass');
// If multiple fleet devices for same member, non-compliant if ANY device fails
if (!isCompliant || !deviceStatusMap[host.member_id]) {
// If multiple fleet devices for same member, non-compliant if ANY device fails.
// Once non-compliant, a later compliant device cannot upgrade it back.
if (deviceStatusMap[host.member_id] !== 'non-compliant') {
deviceStatusMap[host.member_id] = isCompliant ? 'compliant' : 'non-compliant';
}
}
Expand Down
Loading