mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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" },
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user