diff --git a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx index 786f595d32932..6b44ab0911024 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.stories.tsx @@ -1,13 +1,31 @@ import { chromaticWithTablet } from "testHelpers/chromatic"; -import { MockUserMember, MockUserOwner } from "testHelpers/entities"; +import { + MockUserMember, + MockUserOwner, + MockWorkspace, + MockWorkspaceAppStatus, +} from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { userEvent, within } from "storybook/test"; import { NavbarView } from "./NavbarView"; +const tasksFilter = { + username: MockUserOwner.username, +}; + const meta: Meta = { title: "modules/dashboard/NavbarView", - parameters: { chromatic: chromaticWithTablet, layout: "fullscreen" }, + parameters: { + chromatic: chromaticWithTablet, + layout: "fullscreen", + queries: [ + { + key: ["tasks", tasksFilter], + data: [], + }, + ], + }, component: NavbarView, args: { user: MockUserOwner, @@ -78,3 +96,36 @@ export const CustomLogo: Story = { logo_url: "/icon/github.svg", }, }; + +export const IdleTasks: Story = { + parameters: { + queries: [ + { + key: ["tasks", tasksFilter], + data: [ + { + prompt: "Task 1", + workspace: { + ...MockWorkspace, + latest_app_status: { + ...MockWorkspaceAppStatus, + state: "idle", + }, + }, + }, + { + prompt: "Task 2", + workspace: MockWorkspace, + }, + { + prompt: "Task 3", + workspace: { + ...MockWorkspace, + latest_app_status: MockWorkspaceAppStatus, + }, + }, + ], + }, + ], + }, +}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 4a2b3027a47dd..0cafaa8fdd46f 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -1,13 +1,21 @@ import { API } from "api/api"; import type * as TypesGen from "api/typesGenerated"; +import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import type { ProxyContextValue } from "contexts/ProxyContext"; import { useWebpushNotifications } from "contexts/useWebpushNotifications"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; import type { FC } from "react"; +import { useQuery } from "react-query"; import { NavLink, useLocation } from "react-router"; import { cn } from "utils/cn"; import { DeploymentDropdown } from "./DeploymentDropdown"; @@ -17,7 +25,7 @@ import { UserDropdown } from "./UserDropdown/UserDropdown"; interface NavbarViewProps { logo_url?: string; - user?: TypesGen.User; + user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; supportLinks?: readonly TypesGen.LinkConfig[]; onSignOut: () => void; @@ -60,7 +68,7 @@ export const NavbarView: FC = ({ )} - +
{proxyContextValue && ( @@ -109,16 +117,14 @@ export const NavbarView: FC = ({ } /> - {user && ( -
- -
- )} +
+ +
= ({ interface NavItemsProps { className?: string; + user: TypesGen.User; } -const NavItems: FC = ({ className }) => { +const NavItems: FC = ({ className, user }) => { const location = useLocation(); - const { metadata } = useEmbeddedMetadata(); return ( ); }; + +type TasksNavItemProps = { + user: TypesGen.User; +}; + +const TasksNavItem: FC = ({ user }) => { + const { metadata } = useEmbeddedMetadata(); + const canSeeTasks = Boolean( + metadata["tasks-tab-visible"].value || + process.env.NODE_ENV === "development" || + process.env.STORYBOOK, + ); + const filter = { + username: user.username, + }; + const { data: idleCount } = useQuery({ + queryKey: ["tasks", filter], + queryFn: () => API.experimental.getTasks(filter), + refetchInterval: 1_000 * 60, + enabled: canSeeTasks, + refetchOnWindowFocus: true, + initialData: [], + select: (data) => + data.filter((task) => task.workspace.latest_app_status?.state === "idle") + .length, + }); + + if (!canSeeTasks) { + return null; + } + + return ( + { + return cn(linkStyles.default, { [linkStyles.active]: isActive }); + }} + > + Tasks + {idleCount > 0 && ( + + + + + {idleCount} + + + {idleTasksLabel(idleCount)} + + + )} + + ); +}; + +function idleTasksLabel(count: number) { + return `You have ${count} ${count === 1 ? "task" : "tasks"} waiting for input`; +}