diff --git a/site/src/pages/TasksPage/TasksPage.stories.tsx b/site/src/pages/TasksPage/TasksPage.stories.tsx index 4dee0e7fe32a9..9b6179ab9bae2 100644 --- a/site/src/pages/TasksPage/TasksPage.stories.tsx +++ b/site/src/pages/TasksPage/TasksPage.stories.tsx @@ -1,5 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, spyOn, userEvent, within } from "@storybook/test"; +import { API } from "api/api"; +import { MockUsers } from "pages/UsersPage/storybookData/users"; import { MockTemplate, MockUserOwner, @@ -20,6 +22,15 @@ const meta: Meta = { decorators: [withAuthProvider], parameters: { user: MockUserOwner, + permissions: { + viewDeploymentConfig: true, + }, + }, + beforeEach: () => { + spyOn(API, "getUsers").mockResolvedValue({ + users: MockUsers, + count: MockUsers.length, + }); }, }; @@ -62,7 +73,8 @@ export const LoadingTasks: Story = { const canvas = within(canvasElement); await step("Select the first AI template", async () => { - const combobox = await canvas.findByRole("combobox"); + const form = await canvas.findByRole("form"); + const combobox = await within(form).findByRole("combobox"); expect(combobox).toHaveTextContent(MockTemplate.display_name); }); }, @@ -94,37 +106,40 @@ export const LoadedTasks: Story = { }, }; +const newTaskData = { + prompt: "Create a new task", + workspace: { + ...MockWorkspace, + id: "workspace-4", + latest_app_status: { + ...MockWorkspaceAppStatus, + message: "Task created successfully!", + }, + }, +}; + export const CreateTaskSuccessfully: Story = { decorators: [withProxyProvider()], beforeEach: () => { spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); - spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); - spyOn(data, "createTask").mockImplementation((prompt: string) => { - return Promise.resolve({ - prompt, - workspace: { - ...MockWorkspace, - latest_app_status: { - ...MockWorkspaceAppStatus, - message: "Task created successfully!", - }, - }, - }); - }); + spyOn(data, "fetchTasks") + .mockResolvedValueOnce(MockTasks) + .mockResolvedValue([newTaskData, ...MockTasks]); + spyOn(data, "createTask").mockResolvedValue(newTaskData); }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step("Run task", async () => { const prompt = await canvas.findByLabelText(/prompt/i); - await userEvent.type(prompt, "Create a new task"); + await userEvent.type(prompt, newTaskData.prompt); const submitButton = canvas.getByRole("button", { name: /run task/i }); await userEvent.click(submitButton); }); await step("Verify task in the table", async () => { await canvas.findByRole("row", { - name: /create a new task/i, + name: new RegExp(newTaskData.prompt, "i"), }); }); }, @@ -158,6 +173,29 @@ export const CreateTaskError: Story = { }, }; +export const NonAdmin: Story = { + decorators: [withProxyProvider()], + parameters: { + permissions: { + viewDeploymentConfig: false, + }, + }, + beforeEach: () => { + spyOn(data, "fetchAITemplates").mockResolvedValue([MockTemplate]); + spyOn(data, "fetchTasks").mockResolvedValue(MockTasks); + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + + await step("Can't see filters", async () => { + await canvas.findByRole("table"); + expect( + canvas.queryByRole("region", { name: /filters/i }), + ).not.toBeInTheDocument(); + }); + }, +}; + const MockTasks = [ { workspace: { diff --git a/site/src/pages/TasksPage/TasksPage.tsx b/site/src/pages/TasksPage/TasksPage.tsx index 30732a3fe035d..31d5e284b22a6 100644 --- a/site/src/pages/TasksPage/TasksPage.tsx +++ b/site/src/pages/TasksPage/TasksPage.tsx @@ -32,13 +32,18 @@ import { useAuthenticated } from "hooks"; import { ExternalLinkIcon, RotateCcwIcon, SendIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; -import type { FC, ReactNode } from "react"; +import { type FC, type ReactNode, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import TextareaAutosize from "react-textarea-autosize"; import { pageTitle } from "utils/page"; import { relativeTime } from "utils/time"; +import { type UserOption, UsersCombobox } from "./UsersCombobox"; + +type TasksFilter = { + user: UserOption | undefined; +}; const TasksPage: FC = () => { const { @@ -50,6 +55,14 @@ const TasksPage: FC = () => { queryFn: data.fetchAITemplates, ...disabledRefetchOptions, }); + const { user, permissions } = useAuthenticated(); + const [filter, setFilter] = useState({ + user: { + value: user.username, + label: user.name || user.username, + avatarUrl: user.avatar_url, + }, + }); let content: ReactNode = null; @@ -91,7 +104,10 @@ const TasksPage: FC = () => { ) : ( <> - + {permissions.viewDeploymentConfig && ( + + )} + ); } else { @@ -147,12 +163,9 @@ const TaskForm: FC = ({ templates }) => { const createTaskMutation = useMutation({ mutationFn: async ({ prompt, templateId }: CreateTaskMutationFnProps) => data.createTask(prompt, user.id, templateId), - onSuccess: (newTask) => { - // The current data loading is heavy, so we manually update the cache to - // avoid re-fetching. Once we improve data loading, we can replace the - // manual update with queryClient.invalidateQueries. - queryClient.setQueryData(["tasks"], (oldTasks = []) => { - return [newTask, ...oldTasks]; + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ["tasks"], }); }, }); @@ -186,6 +199,7 @@ const TaskForm: FC = ({ templates }) => {