Skip to content

Commit 59a6541

Browse files
refactor: move required external auth buttons to the submit side (#18586)
**Before:** ![Screenshot 2025-06-25 at 14 40 16](https://github.com/user-attachments/assets/cbc558f5-6eee-4133-afc9-2474f04a8a67) **After:** ![Screenshot 2025-06-25 at 14 53 53](https://github.com/user-attachments/assets/3a638f60-d1e4-40a4-a066-8d69fe96c198)
1 parent 2d44add commit 59a6541

File tree

3 files changed

+116
-56
lines changed

3 files changed

+116
-56
lines changed

site/src/hooks/useExternalAuth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ export const useExternalAuth = (versionId: string | undefined) => {
5050
externalAuthPollingState,
5151
isLoadingExternalAuth,
5252
externalAuthError: error,
53+
isPollingExternalAuth: externalAuthPollingState === "polling",
5354
};
5455
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export const MissingExternalAuth: Story = {
245245
});
246246

247247
await step("Renders external authentication", async () => {
248-
await canvas.findByRole("button", { name: /login with github/i });
248+
await canvas.findByRole("button", { name: /connect to github/i });
249249
});
250250
},
251251
};

site/src/pages/TasksPage/TasksPage.tsx

Lines changed: 114 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import Skeleton from "@mui/material/Skeleton";
22
import { API } from "api/api";
33
import { getErrorDetail, getErrorMessage } from "api/errors";
44
import { disabledRefetchOptions } from "api/queries/util";
5-
import type { Template } from "api/typesGenerated";
5+
import type { Template, TemplateVersionExternalAuth } from "api/typesGenerated";
66
import { ErrorAlert } from "components/Alert/ErrorAlert";
77
import { Avatar } from "components/Avatar/Avatar";
88
import { AvatarData } from "components/Avatar/AvatarData";
99
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
1010
import { Button } from "components/Button/Button";
11-
import { Form, FormFields, FormSection } from "components/Form/Form";
1211
import { displayError } from "components/GlobalSnackbar/utils";
1312
import { Margins } from "components/Margins/Margins";
1413
import {
@@ -37,9 +36,16 @@ import {
3736
TableRowSkeleton,
3837
} from "components/TableLoader/TableLoader";
3938

39+
import { ExternalImage } from "components/ExternalImage/ExternalImage";
40+
import {
41+
Tooltip,
42+
TooltipContent,
43+
TooltipProvider,
44+
TooltipTrigger,
45+
} from "components/Tooltip/Tooltip";
4046
import { useAuthenticated } from "hooks";
4147
import { useExternalAuth } from "hooks/useExternalAuth";
42-
import { RotateCcwIcon, SendIcon } from "lucide-react";
48+
import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react";
4349
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
4450
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
4551
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
@@ -50,12 +56,12 @@ import { Link as RouterLink } from "react-router-dom";
5056
import TextareaAutosize from "react-textarea-autosize";
5157
import { pageTitle } from "utils/page";
5258
import { relativeTime } from "utils/time";
53-
import { ExternalAuthButton } from "../CreateWorkspacePage/ExternalAuthButton";
5459
import { type UserOption, UsersCombobox } from "./UsersCombobox";
5560

5661
type TasksFilter = {
5762
user: UserOption | undefined;
5863
};
64+
5965
const TasksPage: FC = () => {
6066
const { user, permissions } = useAuthenticated();
6167
const [filter, setFilter] = useState<TasksFilter>({
@@ -201,21 +207,24 @@ type TaskFormProps = {
201207
const TaskForm: FC<TaskFormProps> = ({ templates }) => {
202208
const { user } = useAuthenticated();
203209
const queryClient = useQueryClient();
204-
205-
const [templateId, setTemplateId] = useState<string>(templates[0].id);
210+
const [selectedTemplateId, setSelectedTemplateId] = useState<string>(
211+
templates[0].id,
212+
);
213+
const selectedTemplate = templates.find(
214+
(t) => t.id === selectedTemplateId,
215+
) as Template;
206216
const {
207217
externalAuth,
208-
externalAuthPollingState,
209-
startPollingExternalAuth,
210-
isLoadingExternalAuth,
211218
externalAuthError,
212-
} = useExternalAuth(
213-
templates.find((t) => t.id === templateId)?.active_version_id,
214-
);
215-
216-
const hasAllRequiredExternalAuth = externalAuth?.every(
217-
(auth) => auth.optional || auth.authenticated,
219+
isPollingExternalAuth,
220+
isLoadingExternalAuth,
221+
} = useExternalAuth(selectedTemplate.active_version_id);
222+
const missedExternalAuth = externalAuth?.filter(
223+
(auth) => !auth.optional && !auth.authenticated,
218224
);
225+
const isMissingExternalAuth = missedExternalAuth
226+
? missedExternalAuth.length > 0
227+
: true;
219228

220229
const createTaskMutation = useMutation({
221230
mutationFn: async ({ prompt, templateId }: CreateTaskMutationFnProps) =>
@@ -235,10 +244,6 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
235244
const prompt = formData.get("prompt") as string;
236245
const templateID = formData.get("templateID") as string;
237246

238-
if (!prompt || !templateID) {
239-
return;
240-
}
241-
242247
try {
243248
await createTaskMutation.mutateAsync({
244249
prompt,
@@ -253,8 +258,12 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
253258
};
254259

255260
return (
256-
<Form onSubmit={onSubmit} aria-label="Create AI task">
257-
{Boolean(externalAuthError) && <ErrorAlert error={externalAuthError} />}
261+
<form
262+
onSubmit={onSubmit}
263+
aria-label="Create AI task"
264+
className="flex flex-col gap-4"
265+
>
266+
{externalAuthError && <ErrorAlert error={externalAuthError} />}
258267

259268
<fieldset
260269
className="border border-border border-solid rounded-lg p-4"
@@ -274,7 +283,7 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
274283
<div className="flex items-center justify-between pt-2">
275284
<Select
276285
name="templateID"
277-
onValueChange={(value) => setTemplateId(value)}
286+
onValueChange={(value) => setSelectedTemplateId(value)}
278287
defaultValue={templates[0].id}
279288
required
280289
>
@@ -294,43 +303,93 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
294303
</SelectContent>
295304
</Select>
296305

297-
<Button
298-
size="sm"
299-
type="submit"
300-
disabled={!hasAllRequiredExternalAuth}
301-
>
302-
<Spinner
303-
loading={createTaskMutation.isPending || isLoadingExternalAuth}
304-
>
305-
<SendIcon />
306-
</Spinner>
307-
Run task
308-
</Button>
306+
<div className="flex items-center gap-2">
307+
{missedExternalAuth && (
308+
<ExternalAuthButtons
309+
template={selectedTemplate}
310+
missedExternalAuth={missedExternalAuth}
311+
/>
312+
)}
313+
314+
<Button size="sm" type="submit" disabled={isMissingExternalAuth}>
315+
<Spinner
316+
loading={
317+
isLoadingExternalAuth ||
318+
isPollingExternalAuth ||
319+
createTaskMutation.isPending
320+
}
321+
>
322+
<SendIcon />
323+
</Spinner>
324+
Run task
325+
</Button>
326+
</div>
309327
</div>
310328
</fieldset>
329+
</form>
330+
);
331+
};
311332

312-
{!hasAllRequiredExternalAuth &&
313-
externalAuth &&
314-
externalAuth.length > 0 && (
315-
<FormSection
316-
title="External Authentication"
317-
description="This template uses external services for authentication."
318-
>
319-
<FormFields>
320-
{externalAuth.map((auth) => (
321-
<ExternalAuthButton
322-
key={auth.id}
323-
auth={auth}
324-
isLoading={externalAuthPollingState === "polling"}
325-
onStartPolling={startPollingExternalAuth}
326-
displayRetry={externalAuthPollingState === "abandoned"}
327-
/>
328-
))}
329-
</FormFields>
330-
</FormSection>
333+
type ExternalAuthButtonProps = {
334+
template: Template;
335+
missedExternalAuth: TemplateVersionExternalAuth[];
336+
};
337+
338+
const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
339+
template,
340+
missedExternalAuth,
341+
}) => {
342+
const {
343+
startPollingExternalAuth,
344+
isPollingExternalAuth,
345+
externalAuthPollingState,
346+
} = useExternalAuth(template.active_version_id);
347+
const shouldRetry = externalAuthPollingState === "abandoned";
348+
349+
return missedExternalAuth.map((auth) => {
350+
return (
351+
<div className="flex items-center gap-2" key={auth.id}>
352+
<Button
353+
variant="outline"
354+
size="sm"
355+
disabled={isPollingExternalAuth || auth.authenticated}
356+
onClick={() => {
357+
window.open(
358+
auth.authenticate_url,
359+
"_blank",
360+
"width=900,height=600",
361+
);
362+
startPollingExternalAuth();
363+
}}
364+
>
365+
<Spinner loading={isPollingExternalAuth}>
366+
<ExternalImage src={auth.display_icon} />
367+
</Spinner>
368+
Connect to {auth.display_name}
369+
</Button>
370+
371+
{shouldRetry && !auth.authenticated && (
372+
<TooltipProvider>
373+
<Tooltip delayDuration={100}>
374+
<TooltipTrigger asChild>
375+
<Button
376+
variant="outline"
377+
size="icon"
378+
onClick={startPollingExternalAuth}
379+
>
380+
<RedoIcon />
381+
<span className="sr-only">Refresh external auth</span>
382+
</Button>
383+
</TooltipTrigger>
384+
<TooltipContent>
385+
Retry connecting to {auth.display_name}
386+
</TooltipContent>
387+
</Tooltip>
388+
</TooltipProvider>
331389
)}
332-
</Form>
333-
);
390+
</div>
391+
);
392+
});
334393
};
335394

336395
type TasksFilterProps = {

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