Skip to content

Commit 6b9a636

Browse files
authored
Merge pull request #2630 from trycompai/main
[comp] Production Deploy
2 parents ca6e1a2 + 6e1a06f commit 6b9a636

10 files changed

Lines changed: 415 additions & 96 deletions

File tree

‎apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.test.tsx‎

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -156,33 +156,43 @@ describe('EmployeeTasks device compliance badge', () => {
156156
expect(screen.getByText('Stale')).toBeInTheDocument();
157157
});
158158

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

167-
rerender(
168-
<EmployeeTasks
169-
employee={baseEmployee}
170-
policies={[]}
171-
trainingVideos={[]}
172-
host={null as unknown as Host}
173-
fleetPolicies={[] as FleetPolicy[]}
174-
organization={baseOrganization}
175-
memberDevice={makeDevice({
176-
complianceStatus: 'stale',
177-
daysSinceLastCheckIn: null,
178-
lastCheckIn: null,
179-
})}
180-
hasHipaaFramework={false}
181-
hipaaCompletedAt={null}
182-
/>,
166+
it('renders the stale-explainer tooltip trigger when daysSinceLastCheckIn is null (never reported)', () => {
167+
renderWithDevice(
168+
makeDevice({
169+
complianceStatus: 'stale',
170+
daysSinceLastCheckIn: null,
171+
lastCheckIn: null,
172+
}),
183173
);
184-
expect(screen.getByText('Stale').closest('[title]')?.getAttribute('title')).toBe(
185-
'No check-ins recorded',
174+
expect(
175+
screen.getByRole('button', { name: /What does Stale mean\?/i }),
176+
).toBeInTheDocument();
177+
});
178+
179+
it('does not render the stale-explainer tooltip trigger for a compliant device', () => {
180+
renderWithDevice(makeDevice({ complianceStatus: 'compliant' }));
181+
expect(
182+
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
183+
).not.toBeInTheDocument();
184+
});
185+
186+
it('does not render the stale-explainer tooltip trigger for a non-compliant device', () => {
187+
renderWithDevice(
188+
makeDevice({
189+
complianceStatus: 'non_compliant',
190+
isCompliant: false,
191+
diskEncryptionEnabled: false,
192+
}),
186193
);
194+
expect(
195+
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
196+
).not.toBeInTheDocument();
187197
});
188198
});

‎apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { TrainingVideo } from '@/lib/data/training-videos';
44
import type { EmployeeTrainingVideoCompletion, Member, Organization, Policy, User } from '@db';
55

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

59-
function staleTitle(daysSinceLastCheckIn: number | null): string {
61+
function staleTooltipCopy(daysSinceLastCheckIn: number | null): string {
6062
return daysSinceLastCheckIn === null
61-
? 'No check-ins recorded'
62-
: `No check-in in ${daysSinceLastCheckIn} days`;
63+
? "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."
64+
: "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.";
6365
}
6466

6567
function DeviceComplianceBadge({ device }: { device: DeviceWithChecks }) {
6668
if (device.complianceStatus === 'stale') {
6769
return (
68-
<Badge variant="secondary" title={staleTitle(device.daysSinceLastCheckIn)}>
69-
{staleLabel(device.daysSinceLastCheckIn)}
70-
</Badge>
70+
<div className="flex items-center gap-1">
71+
<Badge variant="secondary">{staleLabel(device.daysSinceLastCheckIn)}</Badge>
72+
<TooltipProvider>
73+
<Tooltip>
74+
<TooltipTrigger asChild>
75+
<button
76+
type="button"
77+
aria-label="What does Stale mean?"
78+
className="inline-flex items-center text-muted-foreground hover:text-foreground"
79+
onClick={(e) => e.stopPropagation()}
80+
>
81+
<Information size={14} />
82+
</button>
83+
</TooltipTrigger>
84+
<TooltipContent className="max-w-xs text-xs">
85+
{staleTooltipCopy(device.daysSinceLastCheckIn)}
86+
</TooltipContent>
87+
</Tooltip>
88+
</TooltipProvider>
89+
</div>
7190
);
7291
}
7392
if (device.complianceStatus === 'compliant') {

‎apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.test.tsx‎

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ const baseMember = {
6060

6161
const noop = vi.fn();
6262

63-
function renderMemberRow(deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed') {
63+
function renderMemberRow(
64+
deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed',
65+
) {
6466
return render(
6567
<table>
6668
<tbody>
@@ -109,6 +111,41 @@ describe('MemberRow device status', () => {
109111
expect(screen.getByText('Non-Compliant').className).toContain('text-foreground');
110112
});
111113

114+
it('shows "Stale" with gray dot and muted label when deviceStatus is stale', () => {
115+
const { container } = renderMemberRow('stale');
116+
expect(screen.getByText('Stale')).toBeInTheDocument();
117+
expect(screen.getByText('Stale').className).toContain('text-muted-foreground');
118+
expect(container.querySelector('.bg-gray-400')).toBeInTheDocument();
119+
});
120+
121+
it('renders an info tooltip trigger next to the Stale label', () => {
122+
renderMemberRow('stale');
123+
expect(
124+
screen.getByRole('button', { name: /What does Stale mean\?/i }),
125+
).toBeInTheDocument();
126+
});
127+
128+
it('does not render the Stale info tooltip for compliant devices', () => {
129+
renderMemberRow('compliant');
130+
expect(
131+
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
132+
).not.toBeInTheDocument();
133+
});
134+
135+
it('does not render the Stale info tooltip for non-compliant devices', () => {
136+
renderMemberRow('non-compliant');
137+
expect(
138+
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
139+
).not.toBeInTheDocument();
140+
});
141+
142+
it('does not render the Stale info tooltip for not-installed devices', () => {
143+
renderMemberRow('not-installed');
144+
expect(
145+
screen.queryByRole('button', { name: /What does Stale mean\?/i }),
146+
).not.toBeInTheDocument();
147+
});
148+
112149
it('does not show device status for platform admin', () => {
113150
const adminMember = {
114151
...baseMember,
@@ -175,8 +212,13 @@ describe('MemberRow device status', () => {
175212
expect(yellowDot).toBeInTheDocument();
176213
u2();
177214

178-
const { container: c3 } = renderMemberRow('not-installed');
179-
const redDot = c3.querySelector('.bg-red-400');
215+
const { container: c3, unmount: u3 } = renderMemberRow('stale');
216+
const grayDot = c3.querySelector('.bg-gray-400');
217+
expect(grayDot).toBeInTheDocument();
218+
u3();
219+
220+
const { container: c4 } = renderMemberRow('not-installed');
221+
const redDot = c4.querySelector('.bg-red-400');
180222
expect(redDot).toBeInTheDocument();
181223
});
182224

‎apps/app/src/app/(app)/[orgId]/people/all/components/MemberRow.tsx‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
DropdownMenuItem,
2020
DropdownMenuTrigger,
2121
} from '@trycompai/ui/dropdown-menu';
22+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@trycompai/ui/tooltip';
2223
import { parseRolesString } from '@/lib/permissions';
2324
import type { Role } from '@db';
2425
import {
@@ -33,7 +34,7 @@ import {
3334
TableRow,
3435
Text,
3536
} from '@trycompai/design-system';
36-
import { Checkmark, Edit, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons';
37+
import { Checkmark, Edit, Information, Laptop, OverflowMenuVertical, TrashCan } from '@trycompai/design-system/icons';
3738

3839
import { toast } from 'sonner';
3940
import { MultiRoleCombobox } from './MultiRoleCombobox';
@@ -52,7 +53,7 @@ interface MemberRowProps {
5253
isCurrentUserOwner: boolean;
5354
customRoles?: CustomRoleOption[];
5455
taskCompletion?: TaskCompletion;
55-
deviceStatus?: 'compliant' | 'non-compliant' | 'not-installed';
56+
deviceStatus?: 'compliant' | 'non-compliant' | 'stale' | 'not-installed';
5657
isDeviceStatusLoading?: boolean;
5758
}
5859

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

91+
type DeviceStatus = 'compliant' | 'non-compliant' | 'stale' | 'not-installed';
92+
93+
function getDeviceStatusDotClass(status: DeviceStatus): string {
94+
switch (status) {
95+
case 'compliant':
96+
return 'bg-green-500';
97+
case 'non-compliant':
98+
return 'bg-yellow-500';
99+
case 'stale':
100+
return 'bg-gray-400';
101+
case 'not-installed':
102+
return 'bg-red-400';
103+
}
104+
}
105+
106+
function getDeviceStatusLabel(status: DeviceStatus): string {
107+
switch (status) {
108+
case 'compliant':
109+
return 'Compliant';
110+
case 'non-compliant':
111+
return 'Non-Compliant';
112+
case 'stale':
113+
return 'Stale';
114+
case 'not-installed':
115+
return 'Not Installed';
116+
}
117+
}
118+
90119
export function MemberRow({
91120
member,
92121
onRemove,
@@ -251,27 +280,37 @@ export function MemberRow({
251280
) : (
252281
<div className="flex items-center gap-2">
253282
<span
254-
className={`inline-block h-2 w-2 rounded-full ${
255-
deviceStatus === 'compliant'
256-
? 'bg-green-500'
257-
: deviceStatus === 'non-compliant'
258-
? 'bg-yellow-500'
259-
: 'bg-red-400'
260-
}`}
283+
className={`inline-block h-2 w-2 rounded-full ${getDeviceStatusDotClass(deviceStatus)}`}
261284
/>
262285
<span
263286
className={`text-sm ${
264-
deviceStatus === 'not-installed'
287+
deviceStatus === 'not-installed' || deviceStatus === 'stale'
265288
? 'text-muted-foreground'
266289
: 'text-foreground'
267290
}`}
268291
>
269-
{deviceStatus === 'compliant'
270-
? 'Compliant'
271-
: deviceStatus === 'non-compliant'
272-
? 'Non-Compliant'
273-
: 'Not Installed'}
292+
{getDeviceStatusLabel(deviceStatus)}
274293
</span>
294+
{deviceStatus === 'stale' && (
295+
<TooltipProvider>
296+
<Tooltip>
297+
<TooltipTrigger asChild>
298+
<button
299+
type="button"
300+
aria-label="What does Stale mean?"
301+
className="inline-flex items-center text-muted-foreground hover:text-foreground"
302+
onClick={(e) => e.stopPropagation()}
303+
>
304+
<Information size={14} />
305+
</button>
306+
</TooltipTrigger>
307+
<TooltipContent className="max-w-xs text-xs">
308+
This device's CompAI agent hasn't reported in recently, so we can't verify
309+
its current compliance. Ask the employee to update or reinstall the agent.
310+
</TooltipContent>
311+
</Tooltip>
312+
</TooltipProvider>
313+
)}
275314
</div>
276315
)}
277316
</TableCell>

0 commit comments

Comments
 (0)