From 75c8063beac9385af734c007974e73121efbe53d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 3 Jun 2026 03:41:03 +0530 Subject: [PATCH] refactor(automations): improve UI consistency by updating alert messages, enhancing task item layout, and refining timezone selection component --- .../automations/automations-content.tsx | 2 +- .../components/builder/advanced-section.tsx | 8 +- .../builder/automation-builder-form.tsx | 260 ++++++++---------- .../builder/automation-model-fields.tsx | 9 +- .../components/builder/builder-summary.tsx | 107 ++++--- .../components/builder/schedule-section.tsx | 9 +- .../components/builder/task-item.tsx | 21 +- .../components/builder/task-list.tsx | 3 +- .../components/builder/timezone-combobox.tsx | 12 +- .../components/builder/unattended-toggle.tsx | 17 +- .../new/automation-new-content.tsx | 11 +- .../new/components/automation-new-header.tsx | 36 +-- .../layout/ui/sidebar/SidebarUserProfile.tsx | 34 +-- surfsense_web/components/ui/select.tsx | 6 +- 14 files changed, 248 insertions(+), 287 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/automations-content.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/automations-content.tsx index 3a9532f1c..d9c949058 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/automations-content.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/automations-content.tsx @@ -63,7 +63,7 @@ export function AutomationsContent({ searchSpaceId }: AutomationsContentProps) { /> - Couldn't load automations. {error.message} + Couldn't load automations {error.message} ); diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/advanced-section.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/advanced-section.tsx index 740f199af..110de57f6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/advanced-section.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/advanced-section.tsx @@ -58,7 +58,7 @@ export function AdvancedSection({ return (
- + - + - + {BACKOFF_OPTIONS.map((option) => ( {option.label} @@ -105,7 +105,7 @@ export function AdvancedSection({ - + {CONCURRENCY_OPTIONS.map((option) => ( {option.label} diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-builder-form.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-builder-form.tsx index 117b7bfe8..6c4918d68 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-builder-form.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-builder-form.tsx @@ -1,8 +1,8 @@ "use client"; import { useAtomValue } from "jotai"; -import { AlertCircle, Code2, LayoutList, Save } from "lucide-react"; -import Link from "next/link"; +import { AlertCircle, Code2, LayoutList } from "lucide-react"; import { useRouter } from "next/navigation"; +import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import type { z } from "zod"; import { @@ -15,7 +15,9 @@ import { import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { type Automation, @@ -36,7 +38,6 @@ import { hasResolvedModels, hydrateForm, } from "@/lib/automations/builder-schema"; -import { cn } from "@/lib/utils"; import { AdvancedSection } from "./advanced-section"; import { AutomationModelFields } from "./automation-model-fields"; import { BasicsSection } from "./basics-section"; @@ -57,6 +58,7 @@ interface AutomationBuilderFormProps { * eligibility itself is now owned by the in-form pickers. */ submitDisabledReason?: string; + renderModeSwitcher?: (modeSwitcher: ReactNode) => ReactNode; } type Mode = "form" | "json"; @@ -79,6 +81,7 @@ export function AutomationBuilderForm({ searchSpaceId, automation, submitDisabledReason, + renderModeSwitcher, }: AutomationBuilderFormProps) { const router = useRouter(); const { mutateAsync: createAutomation } = useAtomValue(createAutomationMutationAtom); @@ -98,7 +101,7 @@ export function AutomationBuilderForm({ return { mode: "json" as Mode, form: createEmptyForm(), - notice: `This automation ${result.reason}, which the form can't show. Edit it as JSON below.`, + notice: `This automation ${result.reason}, which the form can't show. Edit it as JSON below`, }; } return { mode: "form" as Mode, form: createEmptyForm(), notice: undefined }; @@ -117,11 +120,6 @@ export function AutomationBuilderForm({ const [submitting, setSubmitting] = useState(false); - const cancelHref = - mode === "edit" && automation - ? `/dashboard/${searchSpaceId}/automations/${automation.id}` - : `/dashboard/${searchSpaceId}/automations`; - // Eligible models + the search-space-seeded defaults. Models are chosen per // automation on create; in edit mode the backend preserves the captured // snapshot, so the picker is create-only. @@ -193,7 +191,7 @@ export function AutomationBuilderForm({ // form's own validation enforces completeness on submit. const definition = jsonValue.definition; if (!definition || typeof definition !== "object") { - return { ok: false, issues: [], notice: "Add a definition before switching to the form." }; + return { ok: false, issues: [], notice: "Add a definition before switching to the form" }; } const name = @@ -211,7 +209,7 @@ export function AutomationBuilderForm({ const h = hydrateForm(name, description, definition, triggers); return h.formable ? { ok: true, form: h.form } - : { ok: false, issues: [], notice: `Can't show in the form: it ${h.reason}.` }; + : { ok: false, issues: [], notice: `Can't show in the form: it ${h.reason}` }; } function validateForm(): Record | null { @@ -329,28 +327,44 @@ export function AutomationBuilderForm({ : undefined); // Only gate creation; editing an existing automation isn't blocked here. const submitBlocked = mode === "create" && !!effectiveDisabledReason; + const modeSwitcher = ( + { + if (value === activeMode) return; + if (value === "form") switchToForm(); + else if (value === "json") switchToJson(); + }} + > + + + + Form + + + + Edit as JSON + + + + ); return (
-
-
- (activeMode === "form" ? undefined : switchToForm())} - /> - (activeMode === "json" ? undefined : switchToJson())} - /> -
-
+ {renderModeSwitcher ? ( + renderModeSwitcher(modeSwitcher) + ) : ( +
{modeSwitcher}
+ )} {activeMode === "json" ? ( - + ) : (
-
- - - Basics - - - - - - - - - Tasks - - - patchForm({ tasks })} - /> - patchForm({ unattended })} - /> - - - - - - Schedule - - - patchForm({ schedule })} - onTimezoneChange={(timezone) => patchForm({ timezone })} - /> - - - - - - Models - - - patchForm({ models: { ...form.models, ...patch } })} - /> - - - - - - Settings - - - - patchForm({ execution: { ...form.execution, ...patch } }) - } - onTagsChange={(tags) => patchForm({ tags })} - /> - +
+ +
+ + Basics + + + + +
+ +
+ + Tasks + + + patchForm({ tasks })} + /> + patchForm({ unattended })} + /> + +
+ +
+ + Schedule + + + patchForm({ schedule })} + onTimezoneChange={(timezone) => patchForm({ timezone })} + /> + +
+ +
+ + Models + + + patchForm({ models: { ...form.models, ...patch } })} + /> + +
+ +
+ + Settings + + + + patchForm({ execution: { ...form.execution, ...patch } }) + } + onTagsChange={(tags) => patchForm({ tags })} + /> + +
- + Summary @@ -461,9 +477,6 @@ export function AutomationBuilderForm({ )}
- {submitBlocked ? ( @@ -476,7 +489,6 @@ export function AutomationBuilderForm({ className="cursor-not-allowed opacity-50" onClick={(event) => event.preventDefault()} > - {submitLabel} @@ -491,9 +503,7 @@ export function AutomationBuilderForm({ > {submitting ? ( - ) : ( - - )} + ) : null} {submitLabel} )} @@ -502,34 +512,6 @@ export function AutomationBuilderForm({ ); } -function ModeButton({ - active, - icon: Icon, - label, - onClick, -}: { - active: boolean; - icon: typeof Code2; - label: string; - onClick: () => void; -}) { - return ( - - ); -} - function extractTriggers(raw: unknown): HydratableTrigger[] { if (!Array.isArray(raw)) return []; return raw.map((entry) => { diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-model-fields.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-model-fields.tsx index 6fd0581cd..2c4a0bf60 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-model-fields.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/automation-model-fields.tsx @@ -3,7 +3,7 @@ import { TriangleAlert } from "lucide-react"; import Link from "next/link"; import { memo, useId } from "react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Select, @@ -120,13 +120,12 @@ const ModelSelectField = memo(function ModelSelectField({ + No eligible models - No eligible models. Automations - need a premium or your own (BYOK) model. Set one up in{" "} + Use a premium model or your own (BYOK) model in{" "} role settings - . @@ -155,7 +154,7 @@ const ModelSelectField = memo(function ModelSelectField({ )} - + {premium.length > 0 ? ( Premium diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/builder-summary.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/builder-summary.tsx index 21a77cb5f..55059ab53 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/builder-summary.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/builder-summary.tsx @@ -1,5 +1,5 @@ "use client"; -import { CalendarClock, CheckCircle2, ListOrdered, type LucideIcon, XCircle } from "lucide-react"; +import { Dot } from "lucide-react"; import { type BuilderForm, scheduleToCron } from "@/lib/automations/builder-schema"; import { describeCron } from "@/lib/automations/describe-cron"; @@ -12,85 +12,70 @@ interface BuilderSummaryProps { * chat ``AutomationDraftPreview`` so the two creation paths feel consistent. */ export function BuilderSummary({ form }: BuilderSummaryProps) { - const scheduleLabel = form.schedule - ? `${describeCron(scheduleToCron(form.schedule))} · ${form.timezone}` - : "No schedule — won't run automatically"; + const automationName = form.name.trim() || "Untitled automation"; + const scheduleDescription = form.schedule ? describeCron(scheduleToCron(form.schedule)) : null; + const taskCountLabel = `${form.tasks.length} task${form.tasks.length === 1 ? "" : "s"}`; + const visibleTasks = form.tasks.slice(0, 2); + const hiddenTaskCount = form.tasks.length - visibleTasks.length; return ( -
-
-

{form.name.trim() || "Untitled automation"}

- {form.description?.trim() && ( -

{form.description.trim()}

- )} +
+
+

+ {automationName} +

-
-

{scheduleLabel}

-
+
-
-
    - {form.tasks.map((task, index) => ( -
  1. - - {index + 1} - - - - {task.query.trim() || ( - No instructions yet - )} +
    + + {scheduleDescription ? ( + + {scheduleDescription} + + {form.timezone} + + ) : ( + No schedule — won't run automatically + )} + + + +
      + {visibleTasks.map((task, index) => ( +
    1. + {index + 1}. + + {task.query.trim() || "No instructions yet"} - {task.mentions.length > 0 && ( - - {task.mentions.map((mention) => ( - - @{mention.title} - - ))} - - )} - -
    2. - ))} -
    -
+ + ))} + {hiddenTaskCount > 0 && ( +
  • +{hiddenTaskCount} more tasks
  • + )} + + -
    - {form.unattended ? ( - - ) : ( - - )} - {form.unattended ? "Runs without approval prompts" : "Will reject approval prompts"} + + {form.unattended ? "Runs without approval prompts" : "Approval prompts are rejected"} +
    ); } -function Section({ - icon: Icon, +function SummaryRow({ label, children, }: { - icon: LucideIcon; label: string; children: React.ReactNode; }) { return ( -
    -
    - - {label} -
    - {children} +
    +
    {label}
    +
    {children}
    ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/schedule-section.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/schedule-section.tsx index 401b4f5cb..a207c8c46 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/schedule-section.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/schedule-section.tsx @@ -1,5 +1,5 @@ "use client"; -import { CalendarClock, CalendarOff, Plus, X } from "lucide-react"; +import { CalendarClock, CalendarOff, Dot, Plus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -70,11 +70,12 @@ export function ScheduleSection({ return (
    -
    +
    {label} - · {timezone} + + {timezone}
    diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/timezone-combobox.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/timezone-combobox.tsx index bc3b97542..ed0808bb3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/timezone-combobox.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/timezone-combobox.tsx @@ -35,22 +35,26 @@ export function TimezoneCombobox({ value, onChange }: TimezoneComboboxProps) { variant="outline" role="combobox" aria-expanded={open} - className="w-full justify-between font-normal" + className="w-full justify-between border-popover-border bg-transparent font-normal hover:bg-transparent" > {value || "Select timezone"} - - + + No timezone found. - + {timezones.map((tz) => ( { onChange(tz); setOpen(false); diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/unattended-toggle.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/unattended-toggle.tsx index ba665445f..861f22204 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/unattended-toggle.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/components/builder/unattended-toggle.tsx @@ -1,7 +1,5 @@ "use client"; -import { Info } from "lucide-react"; import { Switch } from "@/components/ui/switch"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; interface UnattendedToggleProps { checked: boolean; @@ -15,26 +13,15 @@ interface UnattendedToggleProps { */ export function UnattendedToggle({ checked, onChange }: UnattendedToggleProps) { return ( -
    +
    Run without asking for approvals - - - - - - Automations run unattended. With this off, any approval the agent asks for is - rejected, which can stall a step. - -

    - Auto-approve actions the agent would normally pause to confirm. + Tasks run automatically without asking for confirmation

    - - - + ( + + )} + /> ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/new/components/automation-new-header.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/new/components/automation-new-header.tsx index ccfbbc9fa..de9e2412b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/new/components/automation-new-header.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/new/components/automation-new-header.tsx @@ -1,38 +1,38 @@ "use client"; -import { ArrowLeft, MessageSquarePlus } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import Link from "next/link"; +import type { ReactNode } from "react"; import { Button } from "@/components/ui/button"; interface AutomationNewHeaderProps { searchSpaceId: number; + modeSwitcher?: ReactNode; } -export function AutomationNewHeader({ searchSpaceId }: AutomationNewHeaderProps) { +export function AutomationNewHeader({ searchSpaceId, modeSwitcher }: AutomationNewHeaderProps) { return (
    - +
    + + {modeSwitcher ?
    {modeSwitcher}
    : null} +

    New automation

    - Set up a task and a schedule. Prefer natural language? Use chat instead. + Configure the task, schedule, and execution settings for this automation.

    - + {modeSwitcher ?
    {modeSwitcher}
    : null}
    ); diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index bc3b36efd..3cecb5504 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -221,18 +221,15 @@ export function SidebarUserProfile({ - -
    - -
    -

    {displayName}

    -

    {user.email}

    -
    + +
    + {/*

    {displayName}

    */} +

    + {user.email} +

    - - {t("user_settings")} @@ -327,7 +324,7 @@ export function SidebarUserProfile({ ))} -

    +

    v{APP_VERSION}

    @@ -406,18 +403,15 @@ export function SidebarUserProfile({ - -
    - -
    -

    {displayName}

    -

    {user.email}

    -
    + +
    +

    {displayName}

    +

    + {user.email} +

    - - {t("user_settings")} @@ -512,7 +506,7 @@ export function SidebarUserProfile({ ))} -

    +

    v{APP_VERSION}

    diff --git a/surfsense_web/components/ui/select.tsx b/surfsense_web/components/ui/select.tsx index cf22bf6a3..a57415c99 100644 --- a/surfsense_web/components/ui/select.tsx +++ b/surfsense_web/components/ui/select.tsx @@ -43,9 +43,12 @@ function SelectTrigger({ function SelectContent({ className, children, + matchTriggerWidth = true, position = "popper", ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + matchTriggerWidth?: boolean; +}) { return (