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
@@ -1,4 +1,4 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, within } from '@testing-library/react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import type { DeviceWithChecks } from '../types';

Expand Down Expand Up @@ -261,9 +261,34 @@ describe('DeviceAgentDevicesList — integration-imported devices', () => {
expect(screen.getByText('Imported Mac')).toBeInTheDocument();

fireEvent.change(screen.getByLabelText('Filter by source'), {
target: { value: 'Kandji' },
target: { value: 'integration:kandji' },
});
expect(screen.queryByText('Agent Mac')).not.toBeInTheDocument();
expect(screen.getByText('Imported Mac')).toBeInTheDocument();
});

it('keeps distinct providers separate in the filter even with the same display name', () => {
render(
<DeviceAgentDevicesList
devices={[
makeIntegrationDevice({
id: 'x',
name: 'Device X',
integrationProvider: { slug: 'kandji', name: 'MDM' },
}),
makeIntegrationDevice({
id: 'y',
name: 'Device Y',
integrationProvider: { slug: 'intune', name: 'MDM' },
}),
]}
/>,
);
const select = screen.getByLabelText('Filter by source');
// "All sources" + two distinct providers (not merged into one "MDM").
expect(within(select).getAllByRole('option')).toHaveLength(3);
fireEvent.change(select, { target: { value: 'integration:intune' } });
expect(screen.queryByText('Device X')).not.toBeInTheDocument();
expect(screen.getByText('Device Y')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
downloadDevicesCsv,
} from '../lib/devices-csv';
import { DeviceTableRow } from './DeviceListCells';
import { sourceLabel } from '../lib/device-source';
import { sourceKey, sourceLabel } from '../lib/device-source';
import { DeviceDetails } from './DeviceDetails';
import { RemoveDeviceAlert } from '../../all/components/RemoveDeviceAlert';

Expand All @@ -55,17 +55,20 @@ export const DeviceAgentDevicesList = ({
const [isRemoveDeviceAlertOpen, setIsRemoveDeviceAlertOpen] = useState(false);
const [isRemovingDevice, setIsRemovingDevice] = useState(false);

// Distinct source labels present, so the filter only offers sources that exist
// (e.g. "Comp Agent", "Kandji").
// Distinct sources present, keyed by a stable id (so two providers that share
// a display name don't collapse into one option) with a label for display.
const sourceOptions = useMemo(() => {
const labels = new Set(devices.map((d) => sourceLabel(d)));
return Array.from(labels).sort();
const byKey = new Map<string, string>();
for (const d of devices) byKey.set(sourceKey(d), sourceLabel(d));
return Array.from(byKey, ([key, label]) => ({ key, label })).sort((a, b) =>
a.label.localeCompare(b.label),
);
}, [devices]);

const filteredDevices = useMemo(() => {
const query = searchQuery.toLowerCase();
return devices.filter((device) => {
if (sourceFilter !== 'all' && sourceLabel(device) !== sourceFilter) {
if (sourceFilter !== 'all' && sourceKey(device) !== sourceFilter) {
return false;
}
if (!query) return true;
Expand Down Expand Up @@ -154,8 +157,8 @@ export const DeviceAgentDevicesList = ({
aria-label="Filter by source"
>
<option value="all">All sources</option>
{sourceOptions.map((label) => (
<option key={label} value={label}>
{sourceOptions.map(({ key, label }) => (
<option key={key} value={key}>
{label}
</option>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,63 +18,19 @@ import {
import { ArrowLeft, Information } from '@trycompai/design-system/icons';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip';
import type { DeviceWithChecks } from '../types';
import {
CHECK_FIELDS,
PLATFORM_LABELS,
isDeviceOnline,
staleLabel,
staleTooltipCopy,
} from '../lib/device-source';
import { NotTrackedBadge } from './DeviceListCells';
import { RevokeAgentAccessDialog } from './RevokeAgentAccessDialog';

const CHECK_FIELDS = [
{ key: 'diskEncryptionEnabled' as const, dbKey: 'disk_encryption', label: 'Disk Encryption' },
{ key: 'antivirusEnabled' as const, dbKey: 'antivirus', label: 'Antivirus' },
{ key: 'passwordPolicySet' as const, dbKey: 'password_policy', label: 'Password Policy' },
{ key: 'screenLockEnabled' as const, dbKey: 'screen_lock', label: 'Screen Lock' },
];

const PLATFORM_LABELS: Record<string, string> = {
macos: 'macOS',
windows: 'Windows',
linux: 'Linux',
};

/** Device is considered online if it checked in within the last 2 hours */
function isDeviceOnline(lastCheckIn: string | null): boolean {
if (!lastCheckIn) return false;
const diffMs = Date.now() - new Date(lastCheckIn).getTime();
return diffMs < 2 * 60 * 60 * 1000;
}

function staleLabel(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`;
}

function staleTooltipCopy(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null
? "This device was registered but hasn't sent a compliance check yet. If it's not new, the agent may not be running or the device may be offline."
: "This device hasn't reported to CompAI in over 7 days, so we can't verify its current compliance. It may be offline, the agent may need to be updated, or the device may no longer be in use. Check with the employee.";
}

function DeviceComplianceBadge({ device }: { device: DeviceWithChecks }) {
if (device.source === 'integration') {
const provider = device.integrationProvider?.name ?? 'an integration';
return (
<div className="flex items-center gap-1">
<Badge variant="secondary">Not tracked</Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Why is compliance not tracked?"
className="inline-flex items-center text-muted-foreground hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<Information size={14} />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-xs">
{`This device was imported from ${provider}. CompAI doesn't collect compliance checks for imported devices — install the CompAI agent to track its security posture.`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
return <NotTrackedBadge device={device} />;
}
if (device.complianceStatus === 'stale') {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,50 +23,17 @@ import {
} from '@trycompai/ui/tooltip';
import Link from 'next/link';
import type { DeviceWithChecks } from '../types';
import { isComplianceTracked, sourceLabel } from '../lib/device-source';

export const CHECK_FIELDS = [
{ key: 'diskEncryptionEnabled' as const, label: 'Disk Encryption' },
{ key: 'antivirusEnabled' as const, label: 'Antivirus' },
{ key: 'passwordPolicySet' as const, label: 'Password Policy' },
{ key: 'screenLockEnabled' as const, label: 'Screen Lock' },
];

export const PLATFORM_LABELS: Record<string, string> = {
macos: 'macOS',
windows: 'Windows',
linux: 'Linux',
};

export function formatTimeAgo(dateString: string | null): string {
if (!dateString) return 'Never';
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));

if (diffHours < 1) return 'Just now';
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}

/** Device is considered online if it checked in within the last 2 hours. */
export function isDeviceOnline(lastCheckIn: string | null): boolean {
if (!lastCheckIn) return false;
const diffMs = Date.now() - new Date(lastCheckIn).getTime();
return diffMs < 2 * 60 * 60 * 1000;
}

function staleLabel(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`;
}

function staleTooltipCopy(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null
? "This device was registered but hasn't sent a compliance check yet. If it's not new, the agent may not be running or the device may be offline."
: "This device hasn't reported to CompAI in over 7 days, so we can't verify its current compliance. It may be offline, the agent may need to be updated, or the device may no longer be in use. Check with the employee.";
}
import {
CHECK_FIELDS,
PLATFORM_LABELS,
formatTimeAgo,
isComplianceTracked,
isDeviceOnline,
notTrackedTooltipCopy,
sourceLabel,
staleLabel,
staleTooltipCopy,
} from '../lib/device-source';

function InfoTooltip({ label, copy }: { label: string; copy: string }) {
return (
Expand Down Expand Up @@ -124,21 +91,28 @@ export function UserNameCell({
);
}

/**
* Compliance badge for an integration-imported device. Shared with the details
* panel so the "Not tracked" copy and provider fallback stay consistent.
*/
export function NotTrackedBadge({ device }: { device: DeviceWithChecks }) {
return (
<div className="flex items-center gap-1">
<Badge variant="secondary">Not tracked</Badge>
<InfoTooltip
label="Why is compliance not tracked?"
copy={notTrackedTooltipCopy(device)}
/>
</div>
);
}

export function CompliantBadge({ device }: { device: DeviceWithChecks }) {
// Integration-imported devices are inventory records, not compliance records —
// CompAI never ran security checks on them, so showing "No" (red) would be a
// false negative. Present them as untracked instead.
if (!isComplianceTracked(device)) {
const provider = device.integrationProvider?.name ?? 'an integration';
return (
<div className="flex items-center gap-1">
<Badge variant="secondary">Not tracked</Badge>
<InfoTooltip
label="Why is compliance not tracked?"
copy={`This device was imported from ${provider}. CompAI doesn't collect compliance checks for imported devices — install the CompAI agent to track its security posture.`}
/>
</div>
);
return <NotTrackedBadge device={device} />;
}

if (device.complianceStatus === 'stale') {
Expand Down
65 changes: 63 additions & 2 deletions apps/app/src/app/(app)/[orgId]/people/devices/lib/device-source.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { DeviceWithChecks } from '../types';

/**
* Human label for where a device came from. Shared by the devices table and the
* CSV export so the two can't drift on source labels.
* Human label for where a device came from. Shared by the devices table, the
* details panel, and the CSV export so they can't drift on source labels.
*/
export function sourceLabel(device: DeviceWithChecks): string {
if (device.source === 'integration') {
Expand All @@ -12,6 +12,18 @@ export function sourceLabel(device: DeviceWithChecks): string {
return 'Comp Agent';
}

/**
* Stable identifier for a device's source — used to key the source filter so two
* distinct providers that happen to share a display name don't collapse into one
* option. (sourceLabel is for display only.)
*/
export function sourceKey(device: DeviceWithChecks): string {
if (device.source === 'integration') {
return `integration:${device.integrationProvider?.slug ?? 'unknown'}`;
}
return device.source; // 'device_agent' | 'fleet'
}

/**
* True for devices whose compliance posture CompAI actually collects. Only
* integration-imported devices are inventory-only ("Not tracked"); agent and
Expand All @@ -20,3 +32,52 @@ export function sourceLabel(device: DeviceWithChecks): string {
export function isComplianceTracked(device: DeviceWithChecks): boolean {
return device.source !== 'integration';
}

// ---------------------------------------------------------------------------
// Shared device presentation helpers (used by both the list and details views
// so copy/labels/thresholds can't drift between them).
// ---------------------------------------------------------------------------

export const PLATFORM_LABELS: Record<string, string> = {
macos: 'macOS',
windows: 'Windows',
linux: 'Linux',
};

export const CHECK_FIELDS = [
{ key: 'diskEncryptionEnabled' as const, dbKey: 'disk_encryption', label: 'Disk Encryption' },
{ key: 'antivirusEnabled' as const, dbKey: 'antivirus', label: 'Antivirus' },
{ key: 'passwordPolicySet' as const, dbKey: 'password_policy', label: 'Password Policy' },
{ key: 'screenLockEnabled' as const, dbKey: 'screen_lock', label: 'Screen Lock' },
];

export function formatTimeAgo(dateString: string | null): string {
if (!dateString) return 'Never';
const diffMs = Date.now() - new Date(dateString).getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours < 1) return 'Just now';
if (diffHours < 24) return `${diffHours}h ago`;
return `${Math.floor(diffHours / 24)}d ago`;
}

/** Device is considered online if it checked in within the last 2 hours. */
export function isDeviceOnline(lastCheckIn: string | null): boolean {
if (!lastCheckIn) return false;
return Date.now() - new Date(lastCheckIn).getTime() < 2 * 60 * 60 * 1000;
}

export function staleLabel(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`;
}

export function staleTooltipCopy(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null
? "This device was registered but hasn't sent a compliance check yet. If it's not new, the agent may not be running or the device may be offline."
: "This device hasn't reported to CompAI in over 7 days, so we can't verify its current compliance. It may be offline, the agent may need to be updated, or the device may no longer be in use. Check with the employee.";
}

/** Tooltip copy for the "Not tracked" badge on integration-imported devices. */
export function notTrackedTooltipCopy(device: DeviceWithChecks): string {
const provider = device.integrationProvider?.name ?? 'an integration';
return `This device was imported from ${provider}. CompAI doesn't collect compliance checks for imported devices — install the CompAI agent to track its security posture.`;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DeviceWithChecks } from '../types';
import { sourceLabel } from './device-source';
import { isComplianceTracked, sourceLabel } from './device-source';

export const DEVICES_CSV_HEADER = [
'Device Name',
Expand Down Expand Up @@ -38,9 +38,9 @@ function yesNo(value: boolean): 'yes' | 'no' {

export function buildDevicesCsv(devices: DeviceWithChecks[]): string {
const rows = devices.map((d) => {
// Only agent devices carry real compliance data. For imported/fleet devices,
// export "not_tracked"/"n/a" rather than a misleading non_compliant + "no".
const tracked = d.source === 'device_agent';
// Integration-imported devices are inventory-only; agent + Fleet carry real
// compliance. Use the shared helper so the CSV matches the rest of the UI.
const tracked = isComplianceTracked(d);
const status = tracked ? d.complianceStatus : 'not_tracked';
const check = (value: boolean) => (tracked ? yesNo(value) : 'n/a');
return [
Expand Down
Loading
Loading