Skip to content

Commit 8f021d0

Browse files
committed
Check external auth before running task
It seems we do not validate external auth in the backend currently, so I opted to do this in the frontend to match the create workspace page.
1 parent 06f4e16 commit 8f021d0

File tree

3 files changed

+176
-12
lines changed

3 files changed

+176
-12
lines changed

site/src/hooks/useExternalAuth.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ export const useExternalAuth = (versionId: string | undefined) => {
1212
setExternalAuthPollingState("polling");
1313
}, []);
1414

15-
const { data: externalAuth, isPending: isLoadingExternalAuth } = useQuery({
15+
const {
16+
data: externalAuth,
17+
isPending: isLoadingExternalAuth,
18+
error,
19+
} = useQuery({
1620
...templateVersionExternalAuth(versionId ?? ""),
1721
enabled: !!versionId,
1822
refetchInterval: externalAuthPollingState === "polling" ? 1000 : false,
@@ -45,5 +49,6 @@ export const useExternalAuth = (versionId: string | undefined) => {
4549
externalAuth,
4650
externalAuthPollingState,
4751
isLoadingExternalAuth,
52+
externalAuthError: error,
4853
};
4954
};

site/src/pages/TasksPage/TasksPage.stories.tsx

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { expect, spyOn, userEvent, within } from "@storybook/test";
2+
import { expect, spyOn, userEvent, waitFor, within } from "@storybook/test";
33
import { API } from "api/api";
44
import { MockUsers } from "pages/UsersPage/storybookData/users";
55
import {
66
MockTemplate,
7+
MockTemplateVersionExternalAuthGithub,
8+
MockTemplateVersionExternalAuthGithubAuthenticated,
79
MockUserOwner,
810
MockWorkspace,
911
MockWorkspaceAppStatus,
@@ -27,10 +29,20 @@ const meta: Meta<typeof TasksPage> = {
2729
},
2830
},
2931
beforeEach: () => {
32+
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([]);
3033
spyOn(API, "getUsers").mockResolvedValue({
3134
users: MockUsers,
3235
count: MockUsers.length,
3336
});
37+
spyOn(data, "fetchAITemplates").mockResolvedValue([
38+
MockTemplate,
39+
{
40+
...MockTemplate,
41+
id: "test-template-2",
42+
name: "template 2",
43+
display_name: "Template 2",
44+
},
45+
]);
3446
},
3547
};
3648

@@ -134,6 +146,7 @@ export const CreateTaskSuccessfully: Story = {
134146
const prompt = await canvas.findByLabelText(/prompt/i);
135147
await userEvent.type(prompt, newTaskData.prompt);
136148
const submitButton = canvas.getByRole("button", { name: /run task/i });
149+
await waitFor(() => expect(submitButton).toBeEnabled());
137150
await userEvent.click(submitButton);
138151
});
139152

@@ -164,6 +177,7 @@ export const CreateTaskError: Story = {
164177
const prompt = await canvas.findByLabelText(/prompt/i);
165178
await userEvent.type(prompt, "Create a new task");
166179
const submitButton = canvas.getByRole("button", { name: /run task/i });
180+
await waitFor(() => expect(submitButton).toBeEnabled());
167181
await userEvent.click(submitButton);
168182
});
169183

@@ -173,6 +187,98 @@ export const CreateTaskError: Story = {
173187
},
174188
};
175189

190+
export const WithExternalAuth: Story = {
191+
decorators: [withProxyProvider()],
192+
beforeEach: () => {
193+
spyOn(data, "fetchTasks")
194+
.mockResolvedValueOnce(MockTasks)
195+
.mockResolvedValue([newTaskData, ...MockTasks]);
196+
spyOn(data, "createTask").mockResolvedValue(newTaskData);
197+
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
198+
MockTemplateVersionExternalAuthGithubAuthenticated,
199+
]);
200+
},
201+
play: async ({ canvasElement, step }) => {
202+
const canvas = within(canvasElement);
203+
204+
await step("Run task", async () => {
205+
const prompt = await canvas.findByLabelText(/prompt/i);
206+
await userEvent.type(prompt, newTaskData.prompt);
207+
const submitButton = canvas.getByRole("button", { name: /run task/i });
208+
await waitFor(() => expect(submitButton).toBeEnabled());
209+
await userEvent.click(submitButton);
210+
});
211+
212+
await step("Verify task in the table", async () => {
213+
await canvas.findByRole("row", {
214+
name: new RegExp(newTaskData.prompt, "i"),
215+
});
216+
});
217+
218+
await step("Does not render external auth", async () => {
219+
expect(
220+
canvas.queryByText(/external authentication/),
221+
).not.toBeInTheDocument();
222+
});
223+
},
224+
};
225+
226+
export const MissingExternalAuth: Story = {
227+
decorators: [withProxyProvider()],
228+
beforeEach: () => {
229+
spyOn(data, "fetchTasks")
230+
.mockResolvedValueOnce(MockTasks)
231+
.mockResolvedValue([newTaskData, ...MockTasks]);
232+
spyOn(data, "createTask").mockResolvedValue(newTaskData);
233+
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
234+
MockTemplateVersionExternalAuthGithub,
235+
]);
236+
},
237+
play: async ({ canvasElement, step }) => {
238+
const canvas = within(canvasElement);
239+
240+
await step("Submit is disabled", async () => {
241+
const prompt = await canvas.findByLabelText(/prompt/i);
242+
await userEvent.type(prompt, newTaskData.prompt);
243+
const submitButton = canvas.getByRole("button", { name: /run task/i });
244+
expect(submitButton).toBeDisabled();
245+
});
246+
247+
await step("Renders external authentication", async () => {
248+
await canvas.findByRole("button", { name: /login with github/i });
249+
});
250+
},
251+
};
252+
253+
export const ExternalAuthError: Story = {
254+
decorators: [withProxyProvider()],
255+
beforeEach: () => {
256+
spyOn(data, "fetchTasks")
257+
.mockResolvedValueOnce(MockTasks)
258+
.mockResolvedValue([newTaskData, ...MockTasks]);
259+
spyOn(data, "createTask").mockResolvedValue(newTaskData);
260+
spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue(
261+
mockApiError({
262+
message: "Failed to load external auth",
263+
}),
264+
);
265+
},
266+
play: async ({ canvasElement, step }) => {
267+
const canvas = within(canvasElement);
268+
269+
await step("Submit is disabled", async () => {
270+
const prompt = await canvas.findByLabelText(/prompt/i);
271+
await userEvent.type(prompt, newTaskData.prompt);
272+
const submitButton = canvas.getByRole("button", { name: /run task/i });
273+
expect(submitButton).toBeDisabled();
274+
});
275+
276+
await step("Renders error", async () => {
277+
await canvas.findByText(/failed to load external auth/i);
278+
});
279+
},
280+
};
281+
176282
export const NonAdmin: Story = {
177283
decorators: [withProxyProvider()],
178284
parameters: {

site/src/pages/TasksPage/TasksPage.tsx

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { API } from "api/api";
22
import { getErrorDetail, getErrorMessage } from "api/errors";
33
import { disabledRefetchOptions } from "api/queries/util";
44
import type { Template } from "api/typesGenerated";
5+
import { ErrorAlert } from "components/Alert/ErrorAlert";
56
import { Avatar } from "components/Avatar/Avatar";
67
import { AvatarData } from "components/Avatar/AvatarData";
78
import { Button } from "components/Button/Button";
9+
import { Form, FormFields, FormSection } from "components/Form/Form";
810
import { displayError } from "components/GlobalSnackbar/utils";
911
import { Margins } from "components/Margins/Margins";
1012
import {
@@ -28,7 +30,9 @@ import {
2830
TableHeader,
2931
TableRow,
3032
} from "components/Table/Table";
33+
3134
import { useAuthenticated } from "hooks";
35+
import { useExternalAuth } from "hooks/useExternalAuth";
3236
import { ExternalLinkIcon, RotateCcwIcon, SendIcon } from "lucide-react";
3337
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
3438
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
@@ -40,6 +44,7 @@ import { Link as RouterLink } from "react-router-dom";
4044
import TextareaAutosize from "react-textarea-autosize";
4145
import { pageTitle } from "utils/page";
4246
import { relativeTime } from "utils/time";
47+
import { ExternalAuthButton } from "../CreateWorkspacePage/ExternalAuthButton";
4348
import { type UserOption, UsersCombobox } from "./UsersCombobox";
4449

4550
type TasksFilter = {
@@ -161,6 +166,21 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
161166
const { user } = useAuthenticated();
162167
const queryClient = useQueryClient();
163168

169+
const [templateId, setTemplateId] = useState<string>(templates[0].id);
170+
const {
171+
externalAuth,
172+
externalAuthPollingState,
173+
startPollingExternalAuth,
174+
isLoadingExternalAuth,
175+
externalAuthError,
176+
} = useExternalAuth(
177+
templates.find((t) => t.id === templateId)?.active_version_id,
178+
);
179+
180+
const hasAllRequiredExternalAuth = externalAuth?.every(
181+
(auth) => auth.optional || auth.authenticated,
182+
);
183+
164184
const createTaskMutation = useMutation({
165185
mutationFn: async ({ prompt, templateId }: CreateTaskMutationFnProps) =>
166186
data.createTask(prompt, user.id, templateId),
@@ -197,12 +217,13 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
197217
};
198218

199219
return (
200-
<form
201-
className="border border-border border-solid rounded-lg p-4"
202-
onSubmit={onSubmit}
203-
aria-label="Create AI task"
204-
>
205-
<fieldset disabled={createTaskMutation.isPending}>
220+
<Form onSubmit={onSubmit} aria-label="Create AI task">
221+
{Boolean(externalAuthError) && <ErrorAlert error={externalAuthError} />}
222+
223+
<fieldset
224+
className="border border-border border-solid rounded-lg p-4"
225+
disabled={createTaskMutation.isPending}
226+
>
206227
<label htmlFor="prompt" className="sr-only">
207228
Prompt
208229
</label>
@@ -215,7 +236,12 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
215236
text-sm shadow-sm text-content-primary placeholder:text-content-secondary md:text-sm`}
216237
/>
217238
<div className="flex items-center justify-between pt-2">
218-
<Select name="templateID" defaultValue={templates[0].id} required>
239+
<Select
240+
name="templateID"
241+
onValueChange={(value) => setTemplateId(value)}
242+
defaultValue={templates[0].id}
243+
required
244+
>
219245
<SelectTrigger className="w-52 text-xs [&_svg]:size-icon-xs border-0 bg-surface-secondary h-8 px-3">
220246
<SelectValue placeholder="Select a template" />
221247
</SelectTrigger>
@@ -232,15 +258,42 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
232258
</SelectContent>
233259
</Select>
234260

235-
<Button size="sm" type="submit">
236-
<Spinner loading={createTaskMutation.isPending}>
261+
<Button
262+
size="sm"
263+
type="submit"
264+
disabled={!hasAllRequiredExternalAuth}
265+
>
266+
<Spinner
267+
loading={createTaskMutation.isPending || isLoadingExternalAuth}
268+
>
237269
<SendIcon />
238270
</Spinner>
239271
Run task
240272
</Button>
241273
</div>
242274
</fieldset>
243-
</form>
275+
276+
{!hasAllRequiredExternalAuth &&
277+
externalAuth &&
278+
externalAuth.length > 0 && (
279+
<FormSection
280+
title="External Authentication"
281+
description="This template uses external services for authentication."
282+
>
283+
<FormFields>
284+
{externalAuth.map((auth) => (
285+
<ExternalAuthButton
286+
key={auth.id}
287+
auth={auth}
288+
isLoading={externalAuthPollingState === "polling"}
289+
onStartPolling={startPollingExternalAuth}
290+
displayRetry={externalAuthPollingState === "abandoned"}
291+
/>
292+
))}
293+
</FormFields>
294+
</FormSection>
295+
)}
296+
</Form>
244297
);
245298
};
246299

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy