feat: add sidebar to task page (#19944)

**Demo:**

https://github.com/user-attachments/assets/83363666-ed0e-488a-84b8-bb46517a2bb8

- Integrate sidebar into the task page
- Minor cosmetic changes
- Fix scrolling issues in tasks list
This commit is contained in:
Bruno Quaresma
2025-09-24 14:52:59 -03:00
committed by GitHub
parent 669984c943
commit 2df9c5b6b5
4 changed files with 72 additions and 46 deletions
@@ -3,6 +3,8 @@ import { getErrorMessage } from "api/errors";
import { cva } from "class-variance-authority";
import { Button } from "components/Button/Button";
import { CoderIcon } from "components/Icons/CoderIcon";
import { ScrollArea } from "components/ScrollArea/ScrollArea";
import { Skeleton } from "components/Skeleton/Skeleton";
import {
Tooltip,
TooltipContent,
@@ -33,7 +35,7 @@ export const TasksSidebar: FC = () => {
className={cn(
"h-full flex flex-col flex-1 min-h-0 gap-6 bg-surface-secondary max-w-80",
"border-solid border-0 border-r transition-all p-3",
{ "max-w-16 items-center": isCollapsed },
{ "max-w-14": isCollapsed },
)}
>
<div className="flex items-center place-content-between">
@@ -41,10 +43,7 @@ export const TasksSidebar: FC = () => {
<Button
size="icon"
variant="subtle"
className={cn([
"size-8 p-0 transition-[margin,opacity]",
"group-data-[collapsible=icon]:-ml-10 group-data-[collapsible=icon]:opacity-0",
])}
className={cn(["size-8 p-0 transition-[margin,opacity]"])}
>
<CoderIcon className="fill-content-primary !size-6 !p-0" />
</Button>
@@ -129,36 +128,34 @@ const TasksSidebarGroup: FC<TasksSidebarGroupProps> = ({ username }) => {
});
return (
<div className="flex flex-col flex-1 gap-2 min-h-0 transition-[opacity] group-data-[collapsible=icon]:opacity-0">
<div className="text-content-secondary text-xs">Tasks</div>
<div className="flex flex-col flex-1 gap-1 min-h-0 overflow-y-auto">
{tasksQuery.data ? (
tasksQuery.data.length > 0 ? (
tasksQuery.data.map((t) => (
<TaskSidebarMenuItem key={t.workspace.id} task={t} />
))
) : (
<ScrollArea>
<div className="flex flex-col gap-2">
<div className="text-content-secondary text-xs">Tasks</div>
<div className="flex flex-col flex-1 gap-1">
{tasksQuery.data ? (
tasksQuery.data.length > 0 ? (
tasksQuery.data.map((t) => (
<TaskSidebarMenuItem key={t.workspace.id} task={t} />
))
) : (
<div className="text-content-secondary text-xs p-4 border-border border-solid rounded text-center">
No tasks found
</div>
)
) : tasksQuery.error ? (
<div className="text-content-secondary text-xs p-4 border-border border-solid rounded text-center">
No tasks found
{getErrorMessage(tasksQuery.error, "Failed to load tasks")}
</div>
)
) : tasksQuery.error ? (
<div className="text-content-secondary text-xs p-4 border-border border-solid rounded text-center">
{getErrorMessage(tasksQuery.error, "Failed to load tasks")}
</div>
) : (
<div className="flex flex-col gap-1">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
aria-hidden={true}
className="h-8 w-full rounded-lg bg-surface-tertiary animate-pulse"
/>
))}
</div>
)}
) : (
<div className="flex flex-col gap-1">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton className="h-8 w-full" key={index} />
))}
</div>
)}
</div>
</div>
</div>
</ScrollArea>
);
};
+16 -1
View File
@@ -2,6 +2,8 @@ import {
MockFailedWorkspace,
MockStartingWorkspace,
MockStoppedWorkspace,
MockTasks,
MockUserOwner,
MockWorkspace,
MockWorkspaceAgentLogSource,
MockWorkspaceAgentReady,
@@ -12,6 +14,7 @@ import {
mockApiError,
} from "testHelpers/entities";
import {
withAuthProvider,
withGlobalSnackbar,
withProxyProvider,
withWebSocket,
@@ -30,9 +33,21 @@ import TaskPage, { data, WorkspaceDoesNotHaveAITaskError } from "./TaskPage";
const meta: Meta<typeof TaskPage> = {
title: "pages/TaskPage",
component: TaskPage,
decorators: [withProxyProvider()],
decorators: [withProxyProvider(), withAuthProvider],
beforeEach: () => {
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
},
parameters: {
layout: "fullscreen",
user: MockUserOwner,
reactRouter: reactRouterParameters({
location: {
pathParams: {
workspace: MockTasks[0].workspace.name,
},
},
routing: { path: "/tasks/:workspace" },
}),
},
};
+26 -12
View File
@@ -17,10 +17,17 @@ import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react";
import { AgentLogs } from "modules/resources/AgentLogs/AgentLogs";
import { useAgentLogs } from "modules/resources/useAgentLogs";
import { TasksSidebar } from "modules/tasks/TasksSidebar/TasksSidebar";
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
import { WorkspaceErrorDialog } from "modules/workspaces/ErrorDialog/WorkspaceErrorDialog";
import { WorkspaceBuildLogs } from "modules/workspaces/WorkspaceBuildLogs/WorkspaceBuildLogs";
import { type FC, type ReactNode, useLayoutEffect, useRef } from "react";
import {
type FC,
type PropsWithChildren,
type ReactNode,
useLayoutEffect,
useRef,
} from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
@@ -35,6 +42,15 @@ import { TaskApps } from "./TaskApps";
import { TaskSidebar } from "./TaskSidebar";
import { TaskTopbar } from "./TaskTopbar";
const TaskPageLayout: FC<PropsWithChildren> = ({ children }) => {
return (
<div className="flex items-stretch h-full">
<TasksSidebar />
<div className="flex flex-col h-full flex-1">{children}</div>
</div>
);
};
const TaskPage = () => {
const { workspace: workspaceName, username } = useParams() as {
workspace: string;
@@ -54,7 +70,7 @@ const TaskPage = () => {
if (error) {
return (
<>
<TaskPageLayout>
<Helmet>
<title>{pageTitle("Error loading task")}</title>
</Helmet>
@@ -81,18 +97,18 @@ const TaskPage = () => {
</div>
</div>
</div>
</>
</TaskPageLayout>
);
}
if (!task) {
return (
<>
<TaskPageLayout>
<Helmet>
<title>{pageTitle("Loading task")}</title>
</Helmet>
<Loader fullscreen />
</>
<Loader className="w-full h-full" />
</TaskPageLayout>
);
}
@@ -142,16 +158,14 @@ const TaskPage = () => {
}
return (
<>
<TaskPageLayout>
<Helmet>
<title>{pageTitle(task.workspace.name)}</title>
</Helmet>
<div className="flex flex-col h-full">
<TaskTopbar task={task} />
{content}
</div>
</>
<TaskTopbar task={task} />
{content}
</TaskPageLayout>
);
};
+1 -1
View File
@@ -22,7 +22,7 @@ type TaskTopbarProps = { task: Task };
export const TaskTopbar: FC<TaskTopbarProps> = ({ task }) => {
return (
<header className="flex flex-shrink-0 items-center px-3 py-4 border-solid border-border border-0 border-b">
<header className="flex flex-shrink-0 items-center p-3 border-solid border-border border-0 border-b">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>