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 @@ -156,33 +156,43 @@ describe('EmployeeTasks device compliance badge', () => {
expect(screen.getByText('Stale')).toBeInTheDocument();
});

it('sets stale badge title tooltip based on daysSinceLastCheckIn', () => {
const { rerender } = renderWithDevice(
makeDevice({ complianceStatus: 'stale', daysSinceLastCheckIn: 9 }),
);
expect(screen.getByText('Stale (9d)').closest('[title]')?.getAttribute('title')).toBe(
'No check-in in 9 days',
);
it('renders a stale-explainer tooltip trigger for a stale device', () => {
renderWithDevice(makeDevice({ complianceStatus: 'stale', daysSinceLastCheckIn: 9 }));
expect(
screen.getByRole('button', { name: /What does Stale mean\?/i }),
).toBeInTheDocument();
});

rerender(
<EmployeeTasks
employee={baseEmployee}
policies={[]}
trainingVideos={[]}
host={null as unknown as Host}
fleetPolicies={[] as FleetPolicy[]}
organization={baseOrganization}
memberDevice={makeDevice({
complianceStatus: 'stale',
daysSinceLastCheckIn: null,
lastCheckIn: null,
})}
hasHipaaFramework={false}
hipaaCompletedAt={null}
/>,
it('renders the stale-explainer tooltip trigger when daysSinceLastCheckIn is null (never reported)', () => {
renderWithDevice(
makeDevice({
complianceStatus: 'stale',
daysSinceLastCheckIn: null,
lastCheckIn: null,
}),
);
expect(screen.getByText('Stale').closest('[title]')?.getAttribute('title')).toBe(
'No check-ins recorded',
expect(
screen.getByRole('button', { name: /What does Stale mean\?/i }),
).toBeInTheDocument();
});

it('does not render the stale-explainer tooltip trigger for a compliant device', () => {
renderWithDevice(makeDevice({ complianceStatus: 'compliant' }));
expect(
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
).not.toBeInTheDocument();
});

it('does not render the stale-explainer tooltip trigger for a non-compliant device', () => {
renderWithDevice(
makeDevice({
complianceStatus: 'non_compliant',
isCompliant: false,
diskEncryptionEnabled: false,
}),
);
expect(
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TrainingVideo } from '@/lib/data/training-videos';
import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db';

import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip';
import {
Badge,
Section,
Expand All @@ -14,6 +15,7 @@ import {
TabsTrigger,
Text,
} from '@trycompai/design-system';
import { Information } from '@trycompai/design-system/icons';
import { AlertCircle, Award, CheckCircle2, Download, Info } from 'lucide-react';
import type { FleetPolicy, Host } from '../../devices/types';
import type { DeviceWithChecks } from '../../devices/types';
Expand Down Expand Up @@ -56,18 +58,35 @@ function staleLabel(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null ? 'Stale' : `Stale (${daysSinceLastCheckIn}d)`;
}

function staleTitle(daysSinceLastCheckIn: number | null): string {
function staleTooltipCopy(daysSinceLastCheckIn: number | null): string {
return daysSinceLastCheckIn === null
? 'No check-ins recorded'
: `No check-in in ${daysSinceLastCheckIn} days`;
? "This device's CompAI agent hasn't reported any check-ins, so we can't verify its current compliance. Ask the employee to install or activate the agent."
: "This device's CompAI agent hasn't reported in over 7 days, so we can't verify its current compliance. Ask the employee to update or reinstall the agent.";
}

function DeviceComplianceBadge({ device }: { device: DeviceWithChecks }) {
if (device.complianceStatus === 'stale') {
return (
<Badge variant="secondary" title={staleTitle(device.daysSinceLastCheckIn)}>
{staleLabel(device.daysSinceLastCheckIn)}
</Badge>
<div className="flex items-center gap-1">
<Badge variant="secondary">{staleLabel(device.daysSinceLastCheckIn)}</Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="What does Stale mean?"
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">
{staleTooltipCopy(device.daysSinceLastCheckIn)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
if (device.complianceStatus === 'compliant') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ const baseMember = {

const noop = vi.fn();

function renderMemberRow(deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed') {
function renderMemberRow(
deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed',
) {
return render(
<table>
<tbody>
Expand Down Expand Up @@ -109,6 +111,41 @@ describe('MemberRow device status', () => {
expect(screen.getByText('Non-Compliant').className).toContain('text-foreground');
});

it('shows "Stale" with gray dot and muted label when deviceStatus is stale', () => {
const { container } = renderMemberRow('stale');
expect(screen.getByText('Stale')).toBeInTheDocument();
expect(screen.getByText('Stale').className).toContain('text-muted-foreground');
expect(container.querySelector('.bg-gray-400')).toBeInTheDocument();
});

it('renders an info tooltip trigger next to the Stale label', () => {
renderMemberRow('stale');
expect(
screen.getByRole('button', { name: /What does Stale mean\?/i }),
).toBeInTheDocument();
});

it('does not render the Stale info tooltip for compliant devices', () => {
renderMemberRow('compliant');
expect(
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
).not.toBeInTheDocument();
});

it('does not render the Stale info tooltip for non-compliant devices', () => {
renderMemberRow('non-compliant');
expect(
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
).not.toBeInTheDocument();
});

it('does not render the Stale info tooltip for not-installed devices', () => {
renderMemberRow('not-installed');
expect(
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
).not.toBeInTheDocument();
});

it('does not show device status for platform admin', () => {
const adminMember = {
...baseMember,
Expand Down Expand Up @@ -175,8 +212,13 @@ describe('MemberRow device status', () => {
expect(yellowDot).toBeInTheDocument();
u2();

const { container: c3 } = renderMemberRow('not-installed');
const redDot = c3.querySelector('.bg-red-400');
const { container: c3, unmount: u3 } = renderMemberRow('stale');
const grayDot = c3.querySelector('.bg-gray-400');
expect(grayDot).toBeInTheDocument();
u3();

const { container: c4 } = renderMemberRow('not-installed');
const redDot = c4.querySelector('.bg-red-400');
expect(redDot).toBeInTheDocument();
});

Expand Down
69 changes: 54 additions & 15 deletions apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@trycompai/ui/dropdown-menu';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip';
import { parseRolesString } from '@/lib/permissions';
import type { Role } from '@db';
import {
Expand All @@ -33,7 +34,7 @@ import {
TableRow,
Text,
} from '@trycompai/design-system';
import { Checkmark, Edit, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons';
import { Checkmark, Edit, Information, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons';

import { toast } from 'sonner';
import { MultiRoleCombobox } from './MultiRoleCombobox';
Expand All @@ -52,7 +53,7 @@ interface MemberRowProps {
isCurrentUserOwner: boolean;
customRoles?: CustomRoleOption[];
taskCompletion?: TaskCompletion;
deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed';
deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed';
isDeviceStatusLoading?: boolean;
}

Expand Down Expand Up @@ -87,6 +88,34 @@ function parseRoles(role: Role | Role[] | string): string[] {
return parseRolesString(role);
}

type DeviceStatus = 'compliant' | 'non-compliant' | 'stale' | 'not-installed';

function getDeviceStatusDotClass(status: DeviceStatus): string {
switch (status) {
case 'compliant':
return 'bg-green-500';
case 'non-compliant':
return 'bg-yellow-500';
case 'stale':
return 'bg-gray-400';
case 'not-installed':
return 'bg-red-400';
}
}

function getDeviceStatusLabel(status: DeviceStatus): string {
switch (status) {
case 'compliant':
return 'Compliant';
case 'non-compliant':
return 'Non-Compliant';
case 'stale':
return 'Stale';
case 'not-installed':
return 'Not Installed';
}
}

export function MemberRow({
member,
onRemove,
Expand Down Expand Up @@ -251,27 +280,37 @@ export function MemberRow({
) : (
<div className="flex items-center gap-2">
<span
className={`inline-block h-2 w-2 rounded-full ${
deviceStatus === 'compliant'
? 'bg-green-500'
: deviceStatus === 'non-compliant'
? 'bg-yellow-500'
: 'bg-red-400'
}`}
className={`inline-block h-2 w-2 rounded-full ${getDeviceStatusDotClass(deviceStatus)}`}
/>
<span
className={`text-sm ${
deviceStatus === 'not-installed'
deviceStatus === 'not-installed' || deviceStatus === 'stale'
? 'text-muted-foreground'
: 'text-foreground'
}`}
>
{deviceStatus === 'compliant'
? 'Compliant'
: deviceStatus === 'non-compliant'
? 'Non-Compliant'
: 'Not Installed'}
{getDeviceStatusLabel(deviceStatus)}
</span>
{deviceStatus === 'stale' && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="What does Stale mean?"
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's CompAI agent hasn't reported in recently, so we can't verify
its current compliance. Ask the employee to update or reinstall the agent.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
</TableCell>
Expand Down
Loading
Loading