Skip to content

Commit 74c6a02

Browse files
authored
Merge pull request #1342 from trycompai/main
[comp] Production Deploy
2 parents 65ebb40 + 5d6db8c commit 74c6a02

File tree

9 files changed

+153
-120
lines changed

9 files changed

+153
-120
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client';
2+
3+
import { cn } from '@comp/ui/cn';
4+
import { useEffect, useMemo, useRef, useState } from 'react';
5+
6+
interface DynamicMinHeightProps {
7+
children: React.ReactNode;
8+
className?: string;
9+
}
10+
11+
export function DynamicMinHeight({ children, className }: DynamicMinHeightProps) {
12+
const containerRef = useRef<HTMLDivElement | null>(null);
13+
const [offsetPx, setOffsetPx] = useState<number>(0);
14+
15+
useEffect(() => {
16+
const headerEl = document.querySelector('header.sticky') as HTMLElement | null;
17+
const bannerEl = document.getElementById('onboarding-banner') as HTMLElement | null;
18+
19+
const compute = () => {
20+
const header = headerEl?.offsetHeight ?? 0;
21+
const banner = bannerEl?.offsetHeight ?? 0;
22+
// Add 1px border for each element like the server calculation did
23+
const extra = 0; // borders already included in offsetHeight
24+
setOffsetPx(header + banner + extra);
25+
};
26+
27+
compute();
28+
29+
const resizeObserver = new ResizeObserver(() => compute());
30+
if (headerEl) resizeObserver.observe(headerEl);
31+
if (bannerEl) resizeObserver.observe(bannerEl);
32+
33+
const onResize = () => compute();
34+
window.addEventListener('resize', onResize);
35+
36+
return () => {
37+
resizeObserver.disconnect();
38+
window.removeEventListener('resize', onResize);
39+
};
40+
}, []);
41+
42+
const style = useMemo(() => ({ minHeight: `calc(100vh - ${offsetPx}px)` }), [offsetPx]);
43+
44+
return (
45+
<div
46+
ref={containerRef}
47+
className={cn('textured-background mx-auto px-4 py-4', className)}
48+
style={style}
49+
>
50+
{children}
51+
</div>
52+
);
53+
}

apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,10 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
197197
}
198198

199199
return (
200-
<Card className="w-full overflow-hidden rounded-none border-x-0 border-t-0">
200+
<Card
201+
id="onboarding-banner"
202+
className="w-full overflow-hidden rounded-none border-x-0 border-t-0"
203+
>
201204
<CardContent className="bg-background flex flex-col items-center justify-center">
202205
<div className="w-full pt-4">{renderStatusContent()}</div>
203206
</CardContent>

apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Control, Task } from '@db';
88
import { BarChart3, Clock } from 'lucide-react';
99
import Link from 'next/link';
1010
import { useParams } from 'next/navigation';
11+
import { computeFrameworkStats } from '../lib/compute';
1112
import type { FrameworkInstanceWithControls } from '../types';
1213

1314
interface FrameworkCardProps {
@@ -51,44 +52,13 @@ export function FrameworkCard({
5152
return 'text-red-600 dark:text-red-400';
5253
};
5354

54-
const controlsCount = frameworkInstance.controls?.length || 0;
55-
const compliantControlsCount = Math.round((complianceScore / 100) * controlsCount);
56-
57-
// Calculate not started controls: controls where all policies are draft or non-existent AND all tasks are todo or non-existent
58-
const notStartedControlsCount =
59-
frameworkInstance.controls?.filter((control) => {
60-
// If a control has no policies and no tasks, it's not started.
61-
const controlTasks = tasks.filter((task) => task.controls.some((c) => c.id === control.id));
62-
63-
if ((!control.policies || control.policies.length === 0) && controlTasks.length === 0) {
64-
return true;
65-
}
66-
67-
// Check if ALL policies are in draft state or non-existent
68-
const policiesNotStarted =
69-
!control.policies ||
70-
control.policies.length === 0 ||
71-
control.policies.every((policy) => policy.status === 'draft');
72-
73-
// Check if ALL tasks are in todo state or there are no tasks
74-
const tasksNotStarted =
75-
controlTasks.length === 0 || controlTasks.every((task) => task.status === 'todo');
76-
77-
return policiesNotStarted && tasksNotStarted;
78-
// If either any policy is not draft or any task is not todo, it's in progress
79-
}).length || 0;
80-
81-
// Calculate in progress controls: Total - Compliant - Not Started
82-
const inProgressCount = Math.max(
83-
0, // Ensure count doesn't go below zero
84-
controlsCount - compliantControlsCount - notStartedControlsCount,
55+
const { totalPolicies, publishedPolicies, totalTasks, doneTasks } = computeFrameworkStats(
56+
frameworkInstance,
57+
tasks,
8558
);
8659

87-
// Use direct framework data:
8860
const frameworkDetails = frameworkInstance.framework;
8961
const statusBadge = getStatusBadge(complianceScore);
90-
91-
// Calculate last activity date - use current date as fallback
9262
const lastActivityDate = new Date().toLocaleDateString('en-US', {
9363
year: 'numeric',
9464
month: 'short',
@@ -129,11 +99,21 @@ export function FrameworkCard({
12999
<Progress value={complianceScore} className="h-1" />
130100
</div>
131101

132-
{/* Stats */}
133-
<div className="text-muted-foreground flex items-center justify-between text-xs">
134-
<span>{compliantControlsCount} complete</span>
135-
<span>{inProgressCount} active</span>
136-
<span>{controlsCount} total</span>
102+
{/* Breakdown */}
103+
<div className="text-muted-foreground space-y-1.5 text-xs">
104+
<div className="flex items-center justify-between">
105+
<span>Policies</span>
106+
<span className="tabular-nums">
107+
{publishedPolicies}/{totalPolicies} published
108+
</span>
109+
</div>
110+
<div className="flex items-center justify-between">
111+
<span>Tasks</span>
112+
<span className="tabular-nums">
113+
{doneTasks}/{totalTasks} done
114+
</span>
115+
</div>
116+
{/* Intentionally omit controls in card to reduce noise */}
137117
</div>
138118

139119
{/* Footer */}

apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkList.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
'use client';
22

33
import { Control, Task } from '@db';
4-
import type { FrameworkInstanceWithControls } from '../types';
54
import { FrameworkCard } from './FrameworkCard';
5+
import type { FrameworkInstanceWithComplianceScore } from './types';
66

77
export function FrameworkList({
8-
frameworksWithControls,
8+
frameworksWithCompliance,
99
tasks,
1010
}: {
11-
frameworksWithControls: FrameworkInstanceWithControls[];
11+
frameworksWithCompliance: FrameworkInstanceWithComplianceScore[];
1212
tasks: (Task & { controls: Control[] })[];
1313
}) {
14-
if (!frameworksWithControls.length) return null;
14+
if (!frameworksWithCompliance.length) return null;
1515

1616
return (
1717
<div className="space-y-6">
18-
{frameworksWithControls.map((frameworkInstance) => (
18+
{frameworksWithCompliance.map(({ frameworkInstance, complianceScore }) => (
1919
<FrameworkCard
2020
key={frameworkInstance.id}
2121
frameworkInstance={frameworkInstance}
22-
complianceScore={0}
22+
complianceScore={complianceScore}
2323
tasks={tasks}
2424
/>
2525
))}

apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ import { useState } from 'react';
1010
import type { FrameworkInstanceWithControls } from '../types';
1111
import { AddFrameworkModal } from './AddFrameworkModal';
1212
import { FrameworkList } from './FrameworkList';
13+
import type { FrameworkInstanceWithComplianceScore } from './types';
1314

1415
export interface FrameworksOverviewProps {
1516
frameworksWithControls: FrameworkInstanceWithControls[];
1617
tasks: (Task & { controls: Control[] })[];
1718
allFrameworks: FrameworkEditorFramework[];
19+
frameworksWithCompliance?: FrameworkInstanceWithComplianceScore[];
1820
}
1921

2022
export function FrameworksOverview({
2123
frameworksWithControls,
2224
tasks,
2325
allFrameworks,
26+
frameworksWithCompliance,
2427
}: FrameworksOverviewProps) {
2528
const params = useParams<{ orgId: string }>();
2629
const organizationId = params.orgId;
@@ -34,7 +37,13 @@ export function FrameworksOverview({
3437
return (
3538
<div className="space-y-4">
3639
<div className="grid w-full gap-4 select-none md:grid-cols-1">
37-
<FrameworkList frameworksWithControls={frameworksWithControls} tasks={tasks} />
40+
<FrameworkList
41+
frameworksWithCompliance={
42+
frameworksWithCompliance ??
43+
frameworksWithControls.map((fw) => ({ frameworkInstance: fw, complianceScore: 0 }))
44+
}
45+
tasks={tasks}
46+
/>
3847
<div className="flex items-center justify-center">
3948
<Button onClick={() => setIsAddFrameworkModalOpen(true)} variant="outline">
4049
{'Add Framework'} <PlusIcon className="h-4 w-4" />

apps/app/src/app/(app)/[orgId]/frameworks/data/getFrameworkWithComplianceScores.ts

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,9 @@
11
'use server';
22

3-
import {
4-
Control, // Policy might still be useful if full Policy objects were ever passed, but selected fields are more common now
5-
type PolicyStatus, // For the selected policy type
6-
type Task,
7-
} from '@db';
3+
import { Control, type Task } from '@db';
84
import { FrameworkInstanceWithComplianceScore } from '../components/types';
9-
import { FrameworkInstanceWithControls } from '../types'; // This now has policies with selected fields
10-
11-
// Define the type for the policies array based on the select in FrameworkInstanceWithControls
12-
type SelectedPolicy = {
13-
id: string;
14-
name: string;
15-
status: PolicyStatus;
16-
};
17-
18-
/**
19-
* Checks if a control is compliant based on its policies and tasks
20-
* @param policies - The policies to check (selected fields)
21-
* @param tasks - The tasks to check
22-
* @returns boolean indicating if all policies and tasks are compliant
23-
*/
24-
const isControlCompliant = (
25-
policies: SelectedPolicy[], // Use the specific selected type
26-
tasks: Task[],
27-
) => {
28-
// If there are no policies, the control is not compliant (or has no policy evidence)
29-
if (!policies || policies.length === 0) {
30-
// Depending on business logic, an empty policies array might mean non-compliant or N/A.
31-
// For now, sticking to original logic of false if empty.
32-
return false;
33-
}
34-
35-
const totalPolicies = policies.length;
36-
const completedPolicies = policies.filter((policy) => {
37-
return policy.status === 'published'; // Directly check status of the selected policy
38-
}).length;
39-
40-
const totalTasks = tasks.length;
41-
const completedTasks = tasks.filter((task) => task.status === 'done').length;
42-
43-
return completedPolicies === totalPolicies && (totalTasks === 0 || completedTasks === totalTasks);
44-
};
5+
import { computeFrameworkStats } from '../lib/compute';
6+
import { FrameworkInstanceWithControls } from '../types';
457

468
/**
479
* Gets all framework instances for an organization with compliance calculations
@@ -52,30 +14,15 @@ export async function getFrameworkWithComplianceScores({
5214
frameworksWithControls,
5315
tasks,
5416
}: {
55-
frameworksWithControls: FrameworkInstanceWithControls[]; // This type defines control.policies as SelectedPolicy[]
17+
frameworksWithControls: FrameworkInstanceWithControls[];
5618
tasks: (Task & { controls: Control[] })[];
5719
}): Promise<FrameworkInstanceWithComplianceScore[]> {
58-
// Calculate compliance for each framework
5920
const frameworksWithComplianceScores = frameworksWithControls.map((frameworkInstance) => {
60-
// Get all controls for this framework
61-
const controls = frameworkInstance.controls;
62-
63-
console.log({ controls });
64-
65-
// Calculate compliance percentage
66-
const totalControls = controls.length;
67-
const compliantControls = controls.filter((control) => {
68-
const controlTasks = tasks.filter((task) => task.controls.some((c) => c.id === control.id));
69-
// control.policies here matches SelectedPolicy[] from FrameworkInstanceWithControls
70-
return isControlCompliant(control.policies, controlTasks);
71-
}).length;
72-
73-
const compliance =
74-
totalControls > 0 ? Math.round((compliantControls / totalControls) * 100) : 0;
21+
const { complianceScore } = computeFrameworkStats(frameworkInstance, tasks);
7522

7623
return {
7724
frameworkInstance,
78-
complianceScore: compliance,
25+
complianceScore: complianceScore,
7926
};
8027
});
8128

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { Control, Task } from '@db';
2+
import type { FrameworkInstanceWithControls } from '../types';
3+
4+
export interface FrameworkStats {
5+
totalPolicies: number;
6+
publishedPolicies: number;
7+
totalTasks: number;
8+
doneTasks: number;
9+
controlsCount: number;
10+
complianceScore: number; // 0-100
11+
}
12+
13+
export function computeFrameworkStats(
14+
frameworkInstance: FrameworkInstanceWithControls,
15+
tasks: (Task & { controls: Control[] })[],
16+
): FrameworkStats {
17+
const controls = frameworkInstance.controls ?? [];
18+
const controlsCount = controls.length;
19+
20+
// Deduplicate policies by id across all controls
21+
const allPolicies = controls.flatMap((c) => c.policies || []);
22+
const uniquePoliciesMap = new Map<string, { id: string; status: string }>();
23+
for (const p of allPolicies) uniquePoliciesMap.set(p.id, p as any);
24+
const uniquePolicies = Array.from(uniquePoliciesMap.values());
25+
26+
const totalPolicies = uniquePolicies.length;
27+
const publishedPolicies = uniquePolicies.filter((p) => p.status === 'published').length;
28+
const policyRatio = totalPolicies > 0 ? publishedPolicies / totalPolicies : 0;
29+
30+
const controlIds = controls.map((c) => c.id);
31+
const frameworkTasks = tasks.filter((t) => t.controls.some((c) => controlIds.includes(c.id)));
32+
const totalTasks = frameworkTasks.length;
33+
const doneTasks = frameworkTasks.filter((t) => t.status === 'done').length;
34+
const taskRatio = totalTasks > 0 ? doneTasks / totalTasks : 1;
35+
36+
const complianceScore = Math.round(((policyRatio + taskRatio) / 2) * 100);
37+
38+
return {
39+
totalPolicies,
40+
publishedPolicies,
41+
totalTasks,
42+
doneTasks,
43+
controlsCount,
44+
complianceScore,
45+
};
46+
}

apps/app/src/app/(app)/[orgId]/frameworks/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { redirect } from 'next/navigation';
77
import { cache } from 'react';
88
import { FrameworksOverview } from './components/FrameworksOverview';
99
import { getAllFrameworkInstancesWithControls } from './data/getAllFrameworkInstancesWithControls';
10+
import { getFrameworkWithComplianceScores } from './data/getFrameworkWithComplianceScores';
1011

1112
export async function generateMetadata() {
1213
return {
@@ -39,6 +40,11 @@ export default async function DashboardPage() {
3940
organizationId,
4041
});
4142

43+
const frameworksWithCompliance = await getFrameworkWithComplianceScores({
44+
frameworksWithControls,
45+
tasks,
46+
});
47+
4248
const allFrameworks = await db.frameworkEditorFramework.findMany({
4349
where: {
4450
visible: true,
@@ -52,6 +58,7 @@ export default async function DashboardPage() {
5258
frameworksWithControls={frameworksWithControls}
5359
tasks={tasks}
5460
allFrameworks={allFrameworks}
61+
frameworksWithCompliance={frameworksWithCompliance}
5562
/>
5663
</PageWithBreadcrumb>
5764
);

0 commit comments

Comments
 (0)