fix(frontend): library page adjustments (#11587)

## Changes 🏗️

### Adjust layout and styles on mobile 📱 

<img width="448" height="843" alt="Screenshot 2025-12-09 at 22 53 14"
src="https://github.com/user-attachments/assets/159bdf4f-e6b2-42f5-8fdf-25f8a62c62d1"
/>

### Make the sidebar cards have contextual actions

<img width="486" height="243" alt="Screenshot 2025-12-09 at 22 53 27"
src="https://github.com/user-attachments/assets/2f530168-3217-47c4-b08d-feccbb9e9152"
/>

Depending on the card type, different type of actions are shown...

### Make buttons in "About agent" card do something

<img width="344" height="346" alt="Screenshot 2025-12-09 at 22 54 01"
src="https://github.com/user-attachments/assets/47181f80-1f68-4ef1-aecc-bbadc7cc9c44"
/>

### Other

- Hide `Schedule` button for agents with trigger run type
- Adjust secondary button background colour...
- Make drawer content scrollable on mobile 

## Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Run locally and test the above

Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com>
This commit is contained in:
Ubbe
2025-12-09 23:17:44 +07:00
committed by GitHub
parent 1305325813
commit c9681f5d44
27 changed files with 850 additions and 128 deletions

View File

@@ -72,7 +72,7 @@ export function NewAgentLibraryView() {
}
return (
<div className="ml-4 grid h-full grid-cols-1 gap-0 pt-3 md:gap-4 lg:grid-cols-[25%_70%]">
<div className="mx-4 grid h-full grid-cols-1 gap-0 pt-3 md:ml-4 md:mr-0 md:gap-4 lg:grid-cols-[25%_70%]">
<SectionWrap className="mb-3 block">
<div
className={cn(

View File

@@ -79,6 +79,8 @@ export function RunAgentModal({
Object.keys(agentInputFields || {}).length > 0 ||
Object.keys(agentCredentialsInputFields || {}).length > 0;
const isTriggerRunType = defaultRunType.includes("trigger");
function handleInputChange(key: string, value: string) {
setInputValues((prev) => ({
...prev,
@@ -153,7 +155,7 @@ export function RunAgentModal({
<Dialog.Footer className="mt-6 bg-white pt-4">
<div className="flex items-center justify-end gap-3">
{!allRequiredInputsAreSet ? (
{isTriggerRunType ? null : !allRequiredInputsAreSet ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>

View File

@@ -1,8 +1,14 @@
"use client";
import { getV1GetGraphVersion } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { ShowMoreText } from "@/components/molecules/ShowMoreText/ShowMoreText";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { exportAsJSONFile } from "@/lib/utils";
import { formatDate } from "@/lib/utils/time";
import Link from "next/link";
import { RunAgentModal } from "../modals/RunAgentModal/RunAgentModal";
import { RunDetailCard } from "../selected-views/RunDetailCard/RunDetailCard";
import { EmptyTasksIllustration } from "./EmptyTasksIllustration";
@@ -12,6 +18,30 @@ type Props = {
};
export function EmptyTasks({ agent }: Props) {
const { toast } = useToast();
async function handleExport() {
try {
const res = await getV1GetGraphVersion(
agent.graph_id,
agent.graph_version,
{ for_export: true },
);
if (res.status === 200) {
const filename = `${agent.name}_v${agent.graph_version}.json`;
exportAsJSONFile(res.data as any, filename);
toast({ title: "Agent exported" });
} else {
toast({ title: "Failed to export agent", variant: "destructive" });
}
} catch (e: any) {
toast({
title: "Failed to export agent",
description: e?.message,
variant: "destructive",
});
}
}
const isPublished = Boolean(agent.marketplace_listing);
const createdAt = formatDate(agent.created_at);
const updatedAt = formatDate(agent.updated_at);
@@ -93,10 +123,15 @@ export function EmptyTasks({ agent }: Props) {
) : null}
</div>
<div className="mt-4 flex items-center gap-2">
<Button variant="secondary" size="small">
Edit agent
<Button variant="secondary" size="small" asChild>
<Link
href={`/build?flowID=${agent.graph_id}&flowVersion=${agent.graph_version}`}
target="_blank"
>
Edit agent
</Link>
</Button>
<Button variant="secondary" size="small">
<Button variant="secondary" size="small" onClick={handleExport}>
Export agent to file
</Button>
</div>

View File

@@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../helpers";
type Props = {
children: React.ReactNode;
};
export function AnchorLinksWrap({ children }: Props) {
return (
<div className={cn(AGENT_LIBRARY_SECTION_PADDING_X, "hidden lg:block")}>
<nav className="flex gap-8 px-3 pb-1">{children}</nav>
</div>
);
}

View File

@@ -0,0 +1,11 @@
type Props = {
children: React.ReactNode;
};
export function SelectedActionsWrap({ children }: Props) {
return (
<div className="my-0 ml-4 flex flex-row items-center gap-3 lg:mx-0 lg:my-4 lg:flex-col">
{children}
</div>
);
}

View File

@@ -13,10 +13,11 @@ import {
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { InfoIcon } from "@phosphor-icons/react";
import { useEffect } from "react";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { AnchorLinksWrap } from "../AnchorLinksWrap";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
@@ -46,6 +47,9 @@ export function SelectedRunView({
const { run, preset, isLoading, responseError, httpError } =
useSelectedRunView(agent.graph_id, runId);
const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint);
const {
pendingReviews,
isLoading: reviewsLoading,
@@ -89,6 +93,15 @@ export function SelectedRunView({
<div className="flex flex-col gap-4">
<RunDetailHeader agent={agent} run={run} />
{!isLgScreenUp ? (
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
) : null}
{preset &&
agent.trigger_setup_info &&
preset.webhook_id &&
@@ -100,38 +113,36 @@ export function SelectedRunView({
)}
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
{withSummary && (
<button
onClick={() => scrollToSection("summary")}
className={anchorStyles}
>
Summary
</button>
)}
<AnchorLinksWrap>
{withSummary && (
<button
onClick={() => scrollToSection("output")}
onClick={() => scrollToSection("summary")}
className={anchorStyles}
>
Output
Summary
</button>
)}
<button
onClick={() => scrollToSection("output")}
className={anchorStyles}
>
Output
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
{withReviews && (
<button
onClick={() => scrollToSection("input")}
onClick={() => scrollToSection("reviews")}
className={anchorStyles}
>
Your input
Reviews ({pendingReviews.length})
</button>
{withReviews && (
<button
onClick={() => scrollToSection("reviews")}
className={anchorStyles}
>
Reviews ({pendingReviews.length})
</button>
)}
</nav>
</div>
)}
</AnchorLinksWrap>
{/* Summary Section */}
{withSummary && (
@@ -216,14 +227,16 @@ export function SelectedRunView({
</div>
</SelectedViewLayout>
</div>
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
{isLgScreenUp ? (
<div className="max-w-[3.75rem] flex-shrink-0">
<SelectedRunActions
agent={agent}
run={run}
onSelectRun={onSelectRun}
onClearSelectedRun={onClearSelectedRun}
/>
</div>
) : null}
</div>
);
}

View File

@@ -12,6 +12,7 @@ import {
StopIcon,
} from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../../AgentActionsDropdown";
import { SelectedActionsWrap } from "../../../SelectedActionsWrap";
import { ShareRunButton } from "../../../ShareRunButton/ShareRunButton";
import { CreateTemplateModal } from "../CreateTemplateModal/CreateTemplateModal";
import { useSelectedRunActions } from "./useSelectedRunActions";
@@ -49,7 +50,7 @@ export function SelectedRunActions(props: Props) {
if (!props.run || !props.agent) return null;
return (
<div className="my-4 flex flex-col items-center gap-3">
<SelectedActionsWrap>
{!isRunning ? (
<Button
variant="icon"
@@ -134,6 +135,6 @@ export function SelectedRunActions(props: Props) {
onCreate={handleCreateTemplate}
run={props.run}
/>
</div>
</SelectedActionsWrap>
);
}

View File

@@ -6,9 +6,10 @@ import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner
import { Text } from "@/components/atoms/Text/Text";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { humanizeCronExpression } from "@/lib/cron-expression-utils";
import { isLargeScreen, useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly";
import { AnchorLinksWrap } from "../AnchorLinksWrap";
import { LoadingSelectedContent } from "../LoadingSelectedContent";
import { RunDetailCard } from "../RunDetailCard/RunDetailCard";
import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader";
@@ -41,6 +42,9 @@ export function SelectedScheduleView({
},
});
const breakpoint = useBreakpoint();
const isLgScreenUp = isLargeScreen(breakpoint);
function scrollToSection(id: string) {
const element = document.getElementById(id);
if (element) {
@@ -83,37 +87,42 @@ export function SelectedScheduleView({
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<SelectedViewLayout agentName={agent.name} agentId={agent.id}>
<div className="flex flex-col gap-4">
<div className="flex w-full items-center justify-between">
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
/>
</div>
<div className="flex w-full flex-col gap-0">
<RunDetailHeader
agent={agent}
run={undefined}
scheduleRecurrence={
schedule
? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}`
: undefined
}
/>
{schedule && !isLgScreenUp ? (
<div className="mt-4">
<SelectedScheduleActions
agent={agent}
scheduleId={schedule.id}
onDeleted={onClearSelectedRun}
/>
</div>
) : null}
</div>
{/* Navigation Links */}
<div className={AGENT_LIBRARY_SECTION_PADDING_X}>
<nav className="flex gap-8 px-3 pb-1">
<button
onClick={() => scrollToSection("schedule")}
className={anchorStyles}
>
Schedule
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
</nav>
</div>
<AnchorLinksWrap>
<button
onClick={() => scrollToSection("schedule")}
className={anchorStyles}
>
Schedule
</button>
<button
onClick={() => scrollToSection("input")}
className={anchorStyles}
>
Your input
</button>
</AnchorLinksWrap>
{/* Schedule Section */}
<div id="schedule" className="scroll-mt-4">
@@ -172,10 +181,6 @@ export function SelectedScheduleView({
<div id="input" className="scroll-mt-4">
<RunDetailCard title="Your input">
<div className="relative">
{/* {// TODO: re-enable edit inputs modal once the API supports it */}
{/* {schedule && Object.keys(schedule.input_data).length > 0 && (
<EditInputsModal agent={agent} schedule={schedule} />
)} */}
<AgentInputsReadOnly
agent={agent}
inputs={schedule?.input_data}
@@ -187,8 +192,8 @@ export function SelectedScheduleView({
</div>
</SelectedViewLayout>
</div>
{schedule ? (
<div className="-mt-2 max-w-[3.75rem] flex-shrink-0">
{schedule && isLgScreenUp ? (
<div className="max-w-[3.75rem] flex-shrink-0">
<SelectedScheduleActions
agent={agent}
scheduleId={schedule.id}

View File

@@ -3,6 +3,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { EyeIcon } from "@phosphor-icons/react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
import { useScheduleDetailHeader } from "../../RunDetailHeader/useScheduleDetailHeader";
import { SelectedActionsWrap } from "../../SelectedActionsWrap";
type Props = {
agent: LibraryAgent;
@@ -19,7 +20,7 @@ export function SelectedScheduleActions({ agent, scheduleId }: Props) {
return (
<>
<div className="my-4 flex flex-col items-center gap-3">
<SelectedActionsWrap>
{openInBuilderHref && (
<Button
variant="icon"
@@ -32,7 +33,7 @@ export function SelectedScheduleActions({ agent, scheduleId }: Props) {
</Button>
)}
<AgentActionsDropdown agent={agent} scheduleId={scheduleId} />
</div>
</SelectedActionsWrap>
</>
);
}

View File

@@ -15,6 +15,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
import { FloppyDiskIcon, PlayIcon, TrashIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
interface Props {
agent: LibraryAgent;
@@ -134,6 +135,7 @@ export function SelectedTemplateActions({
<TrashIcon weight="bold" size={18} />
)}
</Button>
<AgentActionsDropdown agent={agent} />
</div>
<Dialog

View File

@@ -15,6 +15,7 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
import { FloppyDiskIcon, TrashIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { AgentActionsDropdown } from "../../AgentActionsDropdown";
interface Props {
agent: LibraryAgent;
@@ -111,6 +112,7 @@ export function SelectedTriggerActions({
<TrashIcon weight="bold" size={18} />
)}
</Button>
<AgentActionsDropdown agent={agent} />
</div>
<Dialog

View File

@@ -12,7 +12,7 @@ export function SelectedViewLayout(props: Props) {
return (
<SectionWrap className="relative mb-3 flex min-h-0 flex-1 flex-col">
<div
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-4`}
className={`${AGENT_LIBRARY_SECTION_PADDING_X} flex-shrink-0 border-b border-zinc-100 pb-0 lg:pb-4`}
>
<Breadcrumbs
items={[

View File

@@ -14,8 +14,8 @@ import {
} from "@/components/molecules/TabsLine/TabsLine";
import { cn } from "@/lib/utils";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunListItem } from "./components/RunListItem";
import { ScheduleListItem } from "./components/ScheduleListItem";
import { TaskListItem } from "./components/TaskListItem";
import { TemplateListItem } from "./components/TemplateListItem";
import { TriggerListItem } from "./components/TriggerListItem";
import { useSidebarRunsList } from "./useSidebarRunsList";
@@ -112,22 +112,32 @@ export function SidebarRunsList({
}}
className="flex min-h-0 flex-col overflow-hidden"
>
<TabsLineList className={AGENT_LIBRARY_SECTION_PADDING_X}>
<TabsLineTrigger value="runs">
Tasks <span className="ml-3 inline-block">{runsCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="scheduled">
Scheduled <span className="ml-3 inline-block">{schedulesCount}</span>
</TabsLineTrigger>
{triggersCount > 0 && (
<TabsLineTrigger value="triggers">
Triggers <span className="ml-3 inline-block">{triggersCount}</span>
</TabsLineTrigger>
)}
<TabsLineTrigger value="templates">
Templates <span className="ml-3 inline-block">{templatesCount}</span>
</TabsLineTrigger>
</TabsLineList>
<div className="relative overflow-hidden">
<div className="pointer-events-none absolute right-0 top-0 z-10 h-[46px] w-12 bg-gradient-to-l from-[#FAFAFA] to-transparent" />
<div className="scrollbar-hide overflow-x-auto">
<TabsLineList
className={cn(AGENT_LIBRARY_SECTION_PADDING_X, "min-w-max")}
>
<TabsLineTrigger value="runs">
Tasks <span className="ml-3 inline-block">{runsCount}</span>
</TabsLineTrigger>
<TabsLineTrigger value="scheduled">
Scheduled{" "}
<span className="ml-3 inline-block">{schedulesCount}</span>
</TabsLineTrigger>
{triggersCount > 0 && (
<TabsLineTrigger value="triggers">
Triggers{" "}
<span className="ml-3 inline-block">{triggersCount}</span>
</TabsLineTrigger>
)}
<TabsLineTrigger value="templates">
Templates{" "}
<span className="ml-3 inline-block">{templatesCount}</span>
</TabsLineTrigger>
</TabsLineList>
</div>
</div>
<>
<TabsLineContent
@@ -146,9 +156,10 @@ export function SidebarRunsList({
itemWrapperClassName="w-auto lg:w-full"
renderItem={(run) => (
<div className="w-[15rem] lg:w-full">
<RunListItem
<TaskListItem
run={run}
title={agent.name}
agent={agent}
selected={selectedRunId === run.id}
onClick={() => onSelectRun && onSelectRun(run.id, "runs")}
/>
@@ -169,6 +180,7 @@ export function SidebarRunsList({
<div className="w-[15rem] lg:w-full" key={s.id}>
<ScheduleListItem
schedule={s}
agent={agent}
selected={selectedRunId === s.id}
onClick={() => onSelectRun(s.id, "scheduled")}
/>
@@ -197,6 +209,7 @@ export function SidebarRunsList({
<div className="w-[15rem] lg:w-full" key={trigger.id}>
<TriggerListItem
trigger={trigger}
agent={agent}
selected={selectedRunId === trigger.id}
onClick={() => onSelectRun(trigger.id, "triggers")}
/>
@@ -225,6 +238,7 @@ export function SidebarRunsList({
<div className="w-[15rem] lg:w-full" key={template.id}>
<TemplateListItem
template={template}
agent={agent}
selected={selectedRunId === template.id}
onClick={() => onSelectRun(template.id, "templates")}
/>

View File

@@ -0,0 +1,123 @@
"use client";
import {
getGetV1ListExecutionSchedulesForAGraphQueryOptions,
useDeleteV1DeleteExecutionSchedule,
} from "@/app/api/__generated__/endpoints/schedules/schedules";
import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
schedule: GraphExecutionJobInfo;
onDeleted?: () => void;
}
export function ScheduleActionsDropdown({ agent, schedule, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deleteSchedule, isPending: isDeleting } =
useDeleteV1DeleteExecutionSchedule();
async function handleDelete() {
try {
await deleteSchedule({ scheduleId: schedule.id });
toast({ title: "Schedule deleted" });
queryClient.invalidateQueries({
queryKey: getGetV1ListExecutionSchedulesForAGraphQueryOptions(
agent.graph_id,
).queryKey,
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete schedule",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete schedule
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete schedule"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this schedule? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Schedule
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -1,24 +1,30 @@
"use client";
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./RunIconWrapper";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./IconWrapper";
import { ScheduleActionsDropdown } from "./ScheduleActionsDropdown";
import { SidebarItemCard } from "./SidebarItemCard";
interface ScheduleListItemProps {
interface Props {
schedule: GraphExecutionJobInfo;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function ScheduleListItem({
schedule,
agent,
selected,
onClick,
}: ScheduleListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
title={schedule.name}
description={moment(schedule.next_run_time).fromNow()}
onClick={onClick}
@@ -32,6 +38,13 @@ export function ScheduleListItem({
/>
</IconWrapper>
}
actions={
<ScheduleActionsDropdown
agent={agent}
schedule={schedule}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -4,25 +4,27 @@ import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import React from "react";
interface RunListItemProps {
interface Props {
title: string;
description?: string;
icon?: React.ReactNode;
selected?: boolean;
onClick?: () => void;
actions?: React.ReactNode;
}
export function RunSidebarCard({
export function SidebarItemCard({
title,
description,
icon,
selected,
onClick,
}: RunListItemProps) {
actions,
}: Props) {
return (
<button
<div
className={cn(
"w-full rounded-large border border-zinc-200 bg-white p-3 text-left ring-1 ring-transparent transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
"w-full cursor-pointer rounded-large border border-zinc-200 bg-white p-3 text-left ring-1 ring-transparent transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
selected ? "border-slate-800 ring-slate-800" : undefined,
)}
onClick={onClick}
@@ -40,7 +42,10 @@ export function RunSidebarCard({
{description}
</Text>
</div>
{actions ? (
<div onClick={(e) => e.stopPropagation()}>{actions}</div>
) : null}
</div>
</button>
</div>
);
}

View File

@@ -0,0 +1,185 @@
"use client";
import {
getGetV1ListGraphExecutionsInfiniteQueryOptions,
useDeleteV1DeleteGraphExecution,
} from "@/app/api/__generated__/endpoints/graphs/graphs";
import {
getGetV2ListPresetsQueryKey,
usePostV2CreateANewPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { CreateTemplateModal } from "../../../selected-views/SelectedRunView/components/CreateTemplateModal/CreateTemplateModal";
interface Props {
agent: LibraryAgent;
run: GraphExecutionMeta;
onDeleted?: () => void;
}
export function TaskActionsDropdown({ agent, run, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isCreateTemplateModalOpen, setIsCreateTemplateModalOpen] =
useState(false);
const { mutateAsync: deleteRun, isPending: isDeletingRun } =
useDeleteV1DeleteGraphExecution();
const { mutateAsync: createPreset } = usePostV2CreateANewPreset();
async function handleDeleteRun() {
try {
await deleteRun({ graphExecId: run.id });
toast({ title: "Task deleted" });
await queryClient.refetchQueries({
queryKey: getGetV1ListGraphExecutionsInfiniteQueryOptions(
agent.graph_id,
).queryKey,
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete task",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
async function handleCreateTemplate(name: string, description: string) {
try {
const res = await createPreset({
data: {
name,
description,
graph_execution_id: run.id,
},
});
if (res.status === 200) {
toast({
title: "Template created",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setIsCreateTemplateModalOpen(false);
}
} catch (error: unknown) {
toast({
title: "Failed to create template",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setIsCreateTemplateModalOpen(true);
}}
className="flex items-center gap-2"
>
Save as template
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete task
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete task"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this task? This action cannot be
undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeletingRun}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteRun}
loading={isDeletingRun}
>
Delete Task
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
<CreateTemplateModal
isOpen={isCreateTemplateModalOpen}
onClose={() => setIsCreateTemplateModalOpen(false)}
onCreate={handleCreateTemplate}
run={run as any}
/>
</>
);
}

View File

@@ -2,6 +2,7 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import {
CheckCircleIcon,
ClockIcon,
@@ -12,8 +13,9 @@ import {
} from "@phosphor-icons/react";
import moment from "moment";
import React from "react";
import { IconWrapper } from "./RunIconWrapper";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TaskActionsDropdown } from "./TaskActionsDropdown";
const statusIconMap: Record<AgentExecutionStatus, React.ReactNode> = {
INCOMPLETE: (
@@ -53,26 +55,33 @@ const statusIconMap: Record<AgentExecutionStatus, React.ReactNode> = {
),
};
interface RunListItemProps {
interface Props {
run: GraphExecutionMeta;
title: string;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function RunListItem({
export function TaskListItem({
run,
title,
agent,
selected,
onClick,
}: RunListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
icon={statusIconMap[run.status]}
title={title}
description={moment(run.started_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TaskActionsDropdown agent={agent} run={run} onDeleted={onDeleted} />
}
/>
);
}

View File

@@ -0,0 +1,125 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
template: LibraryAgentPreset;
onDeleted?: () => void;
}
export function TemplateActionsDropdown({ agent, template, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deletePreset, isPending: isDeleting } =
useDeleteV2DeleteAPreset();
async function handleDelete() {
try {
await deletePreset({ presetId: template.id });
toast({
title: "Template deleted",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete template",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete template
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete template"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this template? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Template
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -1,24 +1,30 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { FileTextIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./RunIconWrapper";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TemplateActionsDropdown } from "./TemplateActionsDropdown";
interface TemplateListItemProps {
interface Props {
template: LibraryAgentPreset;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function TemplateListItem({
template,
agent,
selected,
onClick,
}: TemplateListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
icon={
<IconWrapper className="border-blue-50 bg-blue-50">
<FileTextIcon size={16} className="text-zinc-700" weight="bold" />
@@ -28,6 +34,13 @@ export function TemplateListItem({
description={moment(template.updated_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TemplateActionsDropdown
agent={agent}
template={template}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -0,0 +1,125 @@
"use client";
import {
getGetV2ListPresetsQueryKey,
useDeleteV2DeleteAPreset,
} from "@/app/api/__generated__/endpoints/presets/presets";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import type { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/molecules/DropdownMenu/DropdownMenu";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
interface Props {
agent: LibraryAgent;
trigger: LibraryAgentPreset;
onDeleted?: () => void;
}
export function TriggerActionsDropdown({ agent, trigger, onDeleted }: Props) {
const { toast } = useToast();
const queryClient = useQueryClient();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const { mutateAsync: deletePreset, isPending: isDeleting } =
useDeleteV2DeleteAPreset();
async function handleDelete() {
try {
await deletePreset({ presetId: trigger.id });
toast({
title: "Trigger deleted",
});
queryClient.invalidateQueries({
queryKey: getGetV2ListPresetsQueryKey({
graph_id: agent.graph_id,
}),
});
setShowDeleteDialog(false);
onDeleted?.();
} catch (error: unknown) {
toast({
title: "Failed to delete trigger",
description:
error instanceof Error
? error.message
: "An unexpected error occurred.",
variant: "destructive",
});
}
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="ml-auto shrink-0 rounded p-1 hover:bg-gray-100"
onClick={(e) => e.stopPropagation()}
aria-label="More actions"
>
<DotsThreeVertical className="h-5 w-5 text-gray-400" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
setShowDeleteDialog(true);
}}
className="flex items-center gap-2"
>
Delete trigger
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
controlled={{
isOpen: showDeleteDialog,
set: setShowDeleteDialog,
}}
styling={{ maxWidth: "32rem" }}
title="Delete trigger"
>
<Dialog.Content>
<div>
<Text variant="large">
Are you sure you want to delete this trigger? This action cannot
be undone.
</Text>
<Dialog.Footer>
<Button
variant="secondary"
disabled={isDeleting}
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
loading={isDeleting}
>
Delete Trigger
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</>
);
}

View File

@@ -1,24 +1,30 @@
"use client";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { LightningIcon } from "@phosphor-icons/react";
import moment from "moment";
import { IconWrapper } from "./RunIconWrapper";
import { RunSidebarCard } from "./RunSidebarCard";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TriggerActionsDropdown } from "./TriggerActionsDropdown";
interface TriggerListItemProps {
interface Props {
trigger: LibraryAgentPreset;
agent: LibraryAgent;
selected?: boolean;
onClick?: () => void;
onDeleted?: () => void;
}
export function TriggerListItem({
trigger,
agent,
selected,
onClick,
}: TriggerListItemProps) {
onDeleted,
}: Props) {
return (
<RunSidebarCard
<SidebarItemCard
icon={
<IconWrapper className="border-purple-50 bg-purple-50">
<LightningIcon size={16} className="text-zinc-700" weight="bold" />
@@ -28,6 +34,13 @@ export function TriggerListItem({
description={moment(trigger.updated_at).fromNow()}
onClick={onClick}
selected={selected}
actions={
<TriggerActionsDropdown
agent={agent}
trigger={trigger}
onDeleted={onDeleted}
/>
}
/>
);
}

View File

@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-slate-50", className)}
className={cn("animate-pulse rounded-md bg-zinc-100", className)}
{...props}
/>
);

View File

@@ -16,7 +16,7 @@ export const extendedButtonVariants = cva(
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
"bg-zinc-200 border-zinc-200 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:

View File

@@ -1,4 +1,6 @@
import { Button } from "@/components/__legacy__/ui/button";
import { scrollbarStyles } from "@/components/styles/scrollbars";
import { cn } from "@/lib/utils";
import { X } from "@phosphor-icons/react";
import { PropsWithChildren } from "react";
import { Drawer } from "vaul";
@@ -41,7 +43,7 @@ export function DrawerWrap({
onInteractOutside={handleClose}
>
<div
className={`flex w-full items-center justify-between ${
className={`flex w-full shrink-0 items-center justify-between ${
title ? "pb-6" : "pb-0"
}`}
>
@@ -61,7 +63,16 @@ export function DrawerWrap({
)
) : null}
</div>
<div>{children}</div>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div
className={cn(
"flex-1 overflow-y-auto overflow-x-hidden",
scrollbarStyles,
)}
>
{children}
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
);

View File

@@ -19,5 +19,5 @@ export const modalStyles = {
// Drawer specific styles
export const drawerStyles = {
...commonStyles,
content: `${commonStyles.content} max-h-[90vh] w-full bottom-0 rounded-br-none rounded-bl-none`,
content: `${commonStyles.content} max-h-[90vh] w-full bottom-0 rounded-br-none rounded-bl-none min-h-0`,
};