Skip to content

Commit 3c8ae73

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 80d1188 commit 3c8ae73

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";
@@ -39,6 +43,7 @@ import { Link as RouterLink } from "react-router-dom";
3943
import TextareaAutosize from "react-textarea-autosize";
4044
import { pageTitle } from "utils/page";
4145
import { relativeTime } from "utils/time";
46+
import { ExternalAuthButton } from "../CreateWorkspacePage/ExternalAuthButton";
4247
import { type UserOption, UsersCombobox } from "./UsersCombobox";
4348

4449
type TasksFilter = {
@@ -160,6 +165,21 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
160165
const { user } = useAuthenticated();
161166
const queryClient = useQueryClient();
162167

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

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

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

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