Skip to content

Commit 04a9e26

Browse files
feat: add advanced mode functionality for organizations (#1503)
- Introduced a new schema for managing the advanced mode setting in organizations. - Implemented an action to update the advanced mode status in the database. - Created a UI component for toggling advanced mode, integrated into the organization settings page. - Updated the main menu and sidebar to conditionally display items based on the advanced mode status. - Added a database migration to include the advancedModeEnabled column in the Organization model. These changes enhance the application's capability to manage advanced features for organizations. Co-authored-by: Mariano Fuentes <marfuen98@gmail.com>
1 parent de11c04 commit 04a9e26

9 files changed

Lines changed: 181 additions & 3 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use server';
2+
3+
import { db } from '@db';
4+
import { revalidatePath, revalidateTag } from 'next/cache';
5+
import { headers } from 'next/headers';
6+
import { authActionClient } from '../safe-action';
7+
import { organizationAdvancedModeSchema } from '../schema';
8+
9+
export const updateOrganizationAdvancedModeAction = authActionClient
10+
.inputSchema(organizationAdvancedModeSchema)
11+
.metadata({
12+
name: 'update-organization-advanced-mode',
13+
track: {
14+
event: 'update-organization-advanced-mode',
15+
channel: 'server',
16+
},
17+
})
18+
.action(async ({ parsedInput, ctx }) => {
19+
const { advancedModeEnabled } = parsedInput;
20+
const { activeOrganizationId } = ctx.session;
21+
22+
if (!activeOrganizationId) {
23+
throw new Error('No active organization');
24+
}
25+
26+
try {
27+
await db.$transaction(async () => {
28+
await db.organization.update({
29+
where: { id: activeOrganizationId },
30+
data: { advancedModeEnabled },
31+
});
32+
});
33+
34+
const headersList = await headers();
35+
let path = headersList.get('x-pathname') || headersList.get('referer') || '';
36+
path = path.replace(/\/[a-z]{2}\//, '/');
37+
38+
revalidatePath(path);
39+
revalidateTag(`organization_${activeOrganizationId}`);
40+
41+
return {
42+
success: true,
43+
};
44+
} catch (error) {
45+
console.error(error);
46+
throw new Error('Failed to update advanced mode setting');
47+
}
48+
});

apps/app/src/actions/schema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export const organizationWebsiteSchema = z.object({
6161
.max(255, 'Website cannot exceed 255 characters'),
6262
});
6363

64+
export const organizationAdvancedModeSchema = z.object({
65+
advancedModeEnabled: z.boolean(),
66+
});
67+
6468
// Risks
6569
export const createRiskSchema = z.object({
6670
title: z

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DeleteOrganization } from '@/components/forms/organization/delete-organization';
2+
import { UpdateOrganizationAdvancedMode } from '@/components/forms/organization/update-organization-advanced-mode';
23
import { UpdateOrganizationName } from '@/components/forms/organization/update-organization-name';
34
import { UpdateOrganizationWebsite } from '@/components/forms/organization/update-organization-website';
45
import { auth } from '@/utils/auth';
@@ -14,6 +15,9 @@ export default async function OrganizationSettings() {
1415
<div className="space-y-4">
1516
<UpdateOrganizationName organizationName={organization?.name ?? ''} />
1617
<UpdateOrganizationWebsite organizationWebsite={organization?.website ?? ''} />
18+
<UpdateOrganizationAdvancedMode
19+
advancedModeEnabled={organization?.advancedModeEnabled ?? false}
20+
/>
1721
<DeleteOrganization organizationId={organization?.id ?? ''} />
1822
</div>
1923
);
@@ -40,6 +44,7 @@ const organizationDetails = cache(async () => {
4044
name: true,
4145
id: true,
4246
website: true,
47+
advancedModeEnabled: true,
4348
},
4449
});
4550

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use client';
2+
3+
import { updateOrganizationAdvancedModeAction } from '@/actions/organization/update-organization-advanced-mode-action';
4+
import { organizationAdvancedModeSchema } from '@/actions/schema';
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardFooter,
10+
CardHeader,
11+
CardTitle,
12+
} from '@comp/ui/card';
13+
import { Form, FormControl, FormField, FormItem, FormMessage } from '@comp/ui/form';
14+
import { Switch } from '@comp/ui/switch';
15+
import { zodResolver } from '@hookform/resolvers/zod';
16+
import { Loader2 } from 'lucide-react';
17+
import { useAction } from 'next-safe-action/hooks';
18+
import { useForm } from 'react-hook-form';
19+
import { toast } from 'sonner';
20+
import type { z } from 'zod';
21+
22+
export function UpdateOrganizationAdvancedMode({
23+
advancedModeEnabled,
24+
}: {
25+
advancedModeEnabled: boolean;
26+
}) {
27+
const updateAdvancedMode = useAction(updateOrganizationAdvancedModeAction, {
28+
onSuccess: () => {
29+
toast.success('Advanced mode setting updated');
30+
},
31+
onError: () => {
32+
toast.error('Error updating advanced mode setting');
33+
},
34+
});
35+
36+
const form = useForm<z.infer<typeof organizationAdvancedModeSchema>>({
37+
resolver: zodResolver(organizationAdvancedModeSchema),
38+
defaultValues: {
39+
advancedModeEnabled,
40+
},
41+
});
42+
43+
const onSubmit = (data: z.infer<typeof organizationAdvancedModeSchema>) => {
44+
updateAdvancedMode.execute(data);
45+
};
46+
47+
return (
48+
<Form {...form}>
49+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
50+
<Card>
51+
<CardHeader>
52+
<CardTitle>Advanced Mode</CardTitle>
53+
<CardDescription>
54+
<div className="max-w-[600px]">
55+
Enable advanced mode to access additional features like the Controls page. This
56+
setting is designed for users who need access to more detailed compliance management
57+
tools.
58+
</div>
59+
</CardDescription>
60+
</CardHeader>
61+
<CardContent>
62+
<FormField
63+
control={form.control}
64+
name="advancedModeEnabled"
65+
render={({ field }) => (
66+
<FormItem className="flex flex-row items-center justify-between rounded-xs border p-3">
67+
<div className="space-y-0.5">
68+
<div className="text-base">Advanced Mode</div>
69+
<div className="text-muted-foreground text-sm">
70+
Show advanced features and pages
71+
</div>
72+
</div>
73+
<FormControl>
74+
<Switch
75+
checked={field.value}
76+
onCheckedChange={(checked) => {
77+
field.onChange(checked);
78+
// Auto-submit when switch is toggled
79+
form.handleSubmit(onSubmit)();
80+
}}
81+
/>
82+
</FormControl>
83+
<FormMessage />
84+
</FormItem>
85+
)}
86+
/>
87+
</CardContent>
88+
<CardFooter className="flex justify-between">
89+
<div className="text-muted-foreground text-xs">
90+
Changes are saved automatically when toggled.
91+
</div>
92+
{updateAdvancedMode.status === 'executing' && (
93+
<div className="flex items-center text-muted-foreground text-sm">
94+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
95+
Saving...
96+
</div>
97+
)}
98+
</CardFooter>
99+
</Card>
100+
</form>
101+
</Form>
102+
);
103+
}

apps/app/src/components/main-menu.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ interface ItemProps {
4444
itemRef: (el: HTMLDivElement | null) => void;
4545
}
4646

47-
export function MainMenu({ organizationId, isCollapsed = false, onItemClick }: Props) {
47+
export function MainMenu({
48+
organizationId,
49+
organization,
50+
isCollapsed = false,
51+
onItemClick,
52+
}: Props) {
4853
const pathname = usePathname();
4954
const [activeStyle, setActiveStyle] = useState({ top: '0px', height: '0px' });
5055
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -65,6 +70,7 @@ export function MainMenu({ organizationId, isCollapsed = false, onItemClick }: P
6570
disabled: false,
6671
icon: ShieldEllipsis,
6772
protected: false,
73+
hidden: !organization?.advancedModeEnabled,
6874
},
6975
{
7076
id: 'policies',
@@ -314,6 +320,7 @@ const Item = ({
314320

315321
type Props = {
316322
organizationId?: string;
323+
organization?: { advancedModeEnabled?: boolean } | null;
317324
isCollapsed?: boolean;
318325
onItemClick?: () => void;
319326
};

apps/app/src/components/mobile-menu.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export function MobileMenu({ organizationId, organizations }: MobileMenuProps) {
4545
organization={currentOrganization}
4646
isCollapsed={false}
4747
/>
48-
<MainMenu organizationId={organizationId} onItemClick={handleCloseSheet} />
48+
<MainMenu
49+
organizationId={organizationId}
50+
organization={currentOrganization}
51+
onItemClick={handleCloseSheet}
52+
/>
4953
</div>
5054
</SheetContent>
5155
</Sheet>

apps/app/src/components/sidebar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export async function Sidebar({
3030
organization={organization}
3131
isCollapsed={isCollapsed}
3232
/>
33-
<MainMenu organizationId={organization?.id ?? ''} isCollapsed={isCollapsed} />
33+
<MainMenu
34+
organizationId={organization?.id ?? ''}
35+
organization={organization}
36+
isCollapsed={isCollapsed}
37+
/>
3438
</div>
3539
</div>
3640
<div className="flex-1" />
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "public"."Organization" ADD COLUMN "advancedModeEnabled" BOOLEAN NOT NULL DEFAULT false;

packages/db/prisma/schema/organization.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ model Organization {
99
website String?
1010
onboardingCompleted Boolean @default(false)
1111
hasAccess Boolean @default(false)
12+
advancedModeEnabled Boolean @default(false)
1213
1314
// FleetDM
1415
fleetDmLabelId Int?

0 commit comments

Comments
 (0)