Skip to content

Commit e147a21

Browse files
committed
chore(browser-automations): implement browser automation configuration and management
1 parent 31bad12 commit e147a21

File tree

13 files changed

+740
-546
lines changed

13 files changed

+740
-546
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import type { Metadata } from 'next';
2+
import { getFeatureFlags } from '@/app/posthog';
3+
import { auth } from '@/utils/auth';
4+
import { headers } from 'next/headers';
5+
import { notFound } from 'next/navigation';
26
import { BrowserConnectionClient } from './components/BrowserConnectionClient';
37

48
export const metadata: Metadata = {
@@ -12,6 +16,21 @@ export default async function BrowserConnectionPage({
1216
}) {
1317
const { orgId } = await params;
1418

19+
const session = await auth.api.getSession({
20+
headers: await headers(),
21+
});
22+
if (!session?.user?.id) {
23+
return notFound();
24+
}
25+
26+
const flags = await getFeatureFlags(session.user.id);
27+
const isWebAutomationsEnabled =
28+
flags['is-web-automations-enabled'] === true || flags['is-web-automations-enabled'] === 'true';
29+
30+
if (!isWebAutomationsEnabled) {
31+
return notFound();
32+
}
33+
1534
return (
1635
<div className="space-y-6">
1736
<div>

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

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getFeatureFlags } from '@/app/posthog';
12
import { auth } from '@/utils/auth';
23
import { SecondaryMenu } from '@comp/ui/secondary-menu';
34
import { headers } from 'next/headers';
@@ -15,37 +16,49 @@ export default async function Layout({ children }: { children: React.ReactNode }
1516
return redirect('/');
1617
}
1718

19+
let isWebAutomationsEnabled = false;
20+
if (session.user?.id) {
21+
const flags = await getFeatureFlags(session.user.id);
22+
isWebAutomationsEnabled =
23+
flags['is-web-automations-enabled'] === true ||
24+
flags['is-web-automations-enabled'] === 'true';
25+
}
26+
27+
const items = [
28+
{
29+
path: `/${orgId}/settings`,
30+
label: 'General',
31+
},
32+
{
33+
path: `/${orgId}/settings/context-hub`,
34+
label: 'Context',
35+
},
36+
{
37+
path: `/${orgId}/settings/api-keys`,
38+
label: 'API',
39+
},
40+
{
41+
path: `/${orgId}/settings/secrets`,
42+
label: 'Secrets',
43+
},
44+
...(isWebAutomationsEnabled
45+
? [
46+
{
47+
path: `/${orgId}/settings/browser-connection`,
48+
label: 'Browser',
49+
},
50+
]
51+
: []),
52+
{
53+
path: `/${orgId}/settings/user`,
54+
label: 'User Settings',
55+
},
56+
] satisfies Array<{ path: string; label: string }>;
57+
1858
return (
1959
<div className="m-auto max-w-[1200px] py-8">
2060
<Suspense fallback={<div>Loading...</div>}>
21-
<SecondaryMenu
22-
items={[
23-
{
24-
path: `/${orgId}/settings`,
25-
label: 'General',
26-
},
27-
{
28-
path: `/${orgId}/settings/context-hub`,
29-
label: 'Context',
30-
},
31-
{
32-
path: `/${orgId}/settings/api-keys`,
33-
label: 'API',
34-
},
35-
{
36-
path: `/${orgId}/settings/secrets`,
37-
label: 'Secrets',
38-
},
39-
{
40-
path: `/${orgId}/settings/browser-connection`,
41-
label: 'Browser',
42-
},
43-
{
44-
path: `/${orgId}/settings/user`,
45-
label: 'User Settings',
46-
},
47-
]}
48-
/>
61+
<SecondaryMenu items={items} />
4962
</Suspense>
5063

5164
<div>{children}</div>

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/BrowserAutomations.tsx

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { useCallback, useEffect, useState } from 'react';
55
import { useBrowserAutomations } from '../hooks/useBrowserAutomations';
66
import { useBrowserContext } from '../hooks/useBrowserContext';
77
import { useBrowserExecution } from '../hooks/useBrowserExecution';
8+
import type { BrowserAutomation } from '../hooks/types';
89
import {
910
BrowserAutomationsList,
1011
BrowserLiveView,
11-
CreateAutomationDialog,
12+
BrowserAutomationConfigDialog,
1213
EmptyWithContextState,
1314
NoContextState,
1415
} from './browser-automations';
@@ -19,7 +20,11 @@ interface BrowserAutomationsProps {
1920

2021
export function BrowserAutomations({ taskId }: BrowserAutomationsProps) {
2122
const { orgId } = useParams<{ orgId: string }>();
22-
const [isCreateOpen, setIsCreateOpen] = useState(false);
23+
const [dialogState, setDialogState] = useState<{
24+
open: boolean;
25+
mode: 'create' | 'edit';
26+
automation?: BrowserAutomation;
27+
}>({ open: false, mode: 'create' });
2328
const [authUrl, setAuthUrl] = useState('https://github.com');
2429

2530
// Hooks
@@ -95,12 +100,17 @@ export function BrowserAutomations({ taskId }: BrowserAutomationsProps) {
95100
if (automations.automations.length === 0) {
96101
return (
97102
<>
98-
<EmptyWithContextState onCreateClick={() => setIsCreateOpen(true)} />
99-
<CreateAutomationDialog
100-
isOpen={isCreateOpen}
101-
onClose={() => setIsCreateOpen(false)}
102-
isCreating={automations.isCreating}
103+
<EmptyWithContextState
104+
onCreateClick={() => setDialogState({ open: true, mode: 'create' })}
105+
/>
106+
<BrowserAutomationConfigDialog
107+
isOpen={dialogState.open}
108+
mode={dialogState.mode}
109+
initialValues={dialogState.automation}
110+
isSaving={automations.isSaving}
111+
onClose={() => setDialogState({ open: false, mode: 'create' })}
103112
onCreate={automations.createAutomation}
113+
onUpdate={automations.updateAutomation}
104114
/>
105115
</>
106116
);
@@ -114,13 +124,19 @@ export function BrowserAutomations({ taskId }: BrowserAutomationsProps) {
114124
hasContext={context.status === 'has-context'}
115125
runningAutomationId={execution.runningAutomationId}
116126
onRun={execution.runAutomation}
117-
onCreateClick={() => setIsCreateOpen(true)}
127+
onCreateClick={() => setDialogState({ open: true, mode: 'create' })}
128+
onEditClick={(automation) =>
129+
setDialogState({ open: true, mode: 'edit', automation })
130+
}
118131
/>
119-
<CreateAutomationDialog
120-
isOpen={isCreateOpen}
121-
onClose={() => setIsCreateOpen(false)}
122-
isCreating={automations.isCreating}
132+
<BrowserAutomationConfigDialog
133+
isOpen={dialogState.open}
134+
mode={dialogState.mode}
135+
initialValues={dialogState.automation}
136+
isSaving={automations.isSaving}
137+
onClose={() => setDialogState({ open: false, mode: 'create' })}
123138
onCreate={automations.createAutomation}
139+
onUpdate={automations.updateAutomation}
124140
/>
125141
</>
126142
);

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/SingleTask.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ interface SingleTaskProps {
5252
initialTask: Task & { fileUrls?: string[]; controls?: Control[] };
5353
initialMembers?: (Member & { user: User })[];
5454
initialAutomations: AutomationWithRuns[];
55+
isWebAutomationsEnabled: boolean;
5556
}
5657

57-
export function SingleTask({ initialTask, initialAutomations }: SingleTaskProps) {
58+
export function SingleTask({
59+
initialTask,
60+
initialAutomations,
61+
isWebAutomationsEnabled,
62+
}: SingleTaskProps) {
5863
const params = useParams();
5964
const orgId = params.orgId as string;
6065

@@ -198,7 +203,7 @@ export function SingleTask({ initialTask, initialAutomations }: SingleTaskProps)
198203
<TaskIntegrationChecks taskId={task.id} onTaskUpdated={() => mutateTask()} />
199204

200205
{/* Browser Automations Section */}
201-
<BrowserAutomations taskId={task.id} />
206+
{isWebAutomationsEnabled && <BrowserAutomations taskId={task.id} />}
202207

203208
{/* Custom Automations Section - only show if no mapped integration checks available */}
204209
{!hasMappedChecks && <TaskAutomations automations={automations || []} />}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client';
2+
3+
import { cn } from '@/lib/utils';
4+
import { Button } from '@comp/ui/button';
5+
import { ChevronDown, Loader2, MonitorPlay, Settings } from 'lucide-react';
6+
import { formatDistanceToNow } from 'date-fns';
7+
import type { BrowserAutomation, BrowserAutomationRun } from '../../hooks/types';
8+
import { RunHistory } from './RunHistory';
9+
10+
interface AutomationItemProps {
11+
automation: BrowserAutomation;
12+
isRunning: boolean;
13+
isExpanded: boolean;
14+
onToggleExpand: () => void;
15+
onRun: () => void;
16+
onEdit: () => void;
17+
}
18+
19+
export function AutomationItem({
20+
automation,
21+
isRunning,
22+
isExpanded,
23+
onToggleExpand,
24+
onRun,
25+
onEdit,
26+
}: AutomationItemProps) {
27+
const runs: BrowserAutomationRun[] = automation.runs || [];
28+
const latestRun = runs[0];
29+
30+
// status dot
31+
const hasFailed = latestRun?.status === 'failed';
32+
const isCompleted = latestRun?.status === 'completed';
33+
const dotColor = hasFailed
34+
? 'bg-destructive shadow-[0_0_8px_rgba(255,0,0,0.3)]'
35+
: isCompleted
36+
? 'bg-primary shadow-[0_0_8px_rgba(0,77,64,0.4)]'
37+
: 'bg-muted-foreground';
38+
39+
return (
40+
<div
41+
className={cn(
42+
'rounded-lg border transition-all duration-300',
43+
isExpanded
44+
? 'border-primary/30 shadow-sm bg-primary/2'
45+
: 'border-border/50 hover:border-border hover:shadow-sm',
46+
)}
47+
>
48+
<div className="flex items-center gap-3 px-4 py-3">
49+
<div className={cn('h-2.5 w-2.5 rounded-full shrink-0', dotColor)} />
50+
51+
<div className="flex-1 min-w-0">
52+
<p className="font-semibold text-foreground text-sm tracking-tight">
53+
{automation.name}
54+
</p>
55+
{latestRun ? (
56+
<p className="text-xs text-muted-foreground mt-0.5">
57+
Last ran {formatDistanceToNow(new Date(latestRun.createdAt), { addSuffix: true })}
58+
</p>
59+
) : (
60+
<p className="text-xs text-muted-foreground mt-0.5">Never run</p>
61+
)}
62+
</div>
63+
64+
<div className="flex items-center gap-2">
65+
<Button variant="ghost" size="icon" onClick={onEdit} aria-label="Edit automation">
66+
<Settings className="h-4 w-4" />
67+
</Button>
68+
69+
<Button variant="outline" size="sm" onClick={onRun} disabled={isRunning}>
70+
{isRunning ? (
71+
<>
72+
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />
73+
Running...
74+
</>
75+
) : (
76+
<>
77+
<MonitorPlay className="mr-1.5 h-3 w-3" />
78+
Run
79+
</>
80+
)}
81+
</Button>
82+
83+
{runs.length > 0 && (
84+
<Button size="sm" variant="ghost" className="h-8 w-8 p-0" onClick={onToggleExpand}>
85+
<ChevronDown
86+
className={cn(
87+
'h-4 w-4 transition-transform duration-300',
88+
isExpanded && 'rotate-180',
89+
)}
90+
/>
91+
</Button>
92+
)}
93+
</div>
94+
</div>
95+
96+
<div
97+
className={cn(
98+
'grid transition-all duration-500 ease-in-out',
99+
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
100+
)}
101+
>
102+
<div className="overflow-hidden">
103+
<div className="px-4 pb-4 pt-2 border-t border-border/50 space-y-4">
104+
<RunHistory runs={runs} />
105+
</div>
106+
</div>
107+
</div>
108+
</div>
109+
);
110+
}
111+
112+

0 commit comments

Comments
 (0)