8000 refactor: move required external auth buttons to the submit side (#18… · coder/coder@59a6541 · GitHub
[go: up one dir, main page]

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 10000 ].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)
0