@@ -2,13 +2,12 @@ import Skeleton from "@mui/material/Skeleton";
2
2
import { API } from "api/api" ;
3
3
import { getErrorDetail , getErrorMessage } from "api/errors" ;
4
4
import { disabledRefetchOptions } from "api/queries/util" ;
5
- import type { Template } from "api/typesGenerated" ;
5
+ import type { Template , TemplateVersionExternalAuth } from "api/typesGenerated" ;
6
6
import { ErrorAlert } from "components/Alert/ErrorAlert" ;
7
7
import { Avatar } from "components/Avatar/Avatar" ;
8
8
import { AvatarData } from "components/Avatar/AvatarData" ;
9
9
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton" ;
10
10
import { Button } from "components/Button/Button" ;
11
- import { Form , FormFields , FormSection } from "components/Form/Form" ;
12
11
import { displayError } from "components/GlobalSnackbar/utils" ;
13
12
import { Margins } from "components/Margins/Margins" ;
14
13
import {
@@ -37,9 +36,16 @@ import {
37
36
TableRowSkeleton ,
38
37
} from "components/TableLoader/TableLoader" ;
39
38
39
+ import { ExternalImage } from "components/ExternalImage/ExternalImage" ;
40
+ import {
41
+ Tooltip ,
42
+ TooltipContent ,
43
+ TooltipProvider ,
44
+ TooltipTrigger ,
45
+ } from "components/Tooltip/Tooltip" ;
40
46
import { useAuthenticated } from "hooks" ;
41
47
import { useExternalAuth } from "hooks/useExternalAuth" ;
42
- import { RotateCcwIcon , SendIcon } from "lucide-react" ;
48
+ import { RedoIcon , RotateCcwIcon , SendIcon } from "lucide-react" ;
43
49
import { AI_PROMPT_PARAMETER_NAME , type Task } from "modules/tasks/tasks" ;
44
50
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus" ;
45
51
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName" ;
@@ -50,12 +56,12 @@ import { Link as RouterLink } from "react-router-dom";
50
56
import TextareaAutosize from "react-textarea-autosize" ;
51
57
import { pageTitle } from "utils/page" ;
52
58
import { relativeTime } from "utils/time" ;
53
- import { ExternalAuthButton } from "../CreateWorkspacePage/ExternalAuthButton" ;
54
59
import { type UserOption , UsersCombobox } from "./UsersCombobox" ;
55
60
56
61
type TasksFilter = {
57
62
user : UserOption | undefined ;
58
63
} ;
64
+
59
65
const TasksPage : FC = ( ) => {
60
66
const { user, permissions } = useAuthenticated ( ) ;
61
67
const [ filter , setFilter ] = useState < TasksFilter > ( {
@@ -201,21 +207,24 @@ type TaskFormProps = {
201
207
const TaskForm : FC < TaskFormProps > = ( { templates } ) => {
202
208
const { user } = useAuthenticated ( ) ;
203
209
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 ;
206
216
const {
207
217
externalAuth,
208
- externalAuthPollingState,
209
- startPollingExternalAuth,
210
- isLoadingExternalAuth,
211
218
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 ,
218
224
) ;
225
+ const isMissingExternalAuth = missedExternalAuth
226
+ ? missedExternalAuth . length > 0
227
+ : true ;
219
228
220
229
const createTaskMutation = useMutation ( {
221
230
mutationFn : async ( { prompt, templateId } : CreateTaskMutationFnProps ) =>
@@ -235,10 +244,6 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
235
244
const prompt = formData . get ( "prompt" ) as string ;
236
245
const templateID = formData . get ( "templateID" ) as string ;
237
246
238
- if ( ! prompt || ! templateID ) {
239
- return ;
240
- }
241
-
242
247
try {
243
248
await createTaskMutation . mutateAsync ( {
244
249
prompt,
@@ -253,8 +258,12 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
253
258
} ;
254
259
255
260
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 } /> }
258
267
259
268
< fieldset
260
269
className = "border border-border border-solid rounded-lg p-4"
@@ -274,7 +283,7 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
274
283
< div className = "flex items-center justify-between pt-2" >
275
284
< Select
276
285
name = "templateID"
277
- onValueChange = { ( value ) => setTemplateId ( value ) }
286
+ onValueChange = { ( value ) => setSelectedTemplateId ( value ) }
278
287
defaultValue = { templates [ 0
10000
] . id }
279
288
required
280
289
>
@@ -294,43 +303,93 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
294
303
</ SelectContent >
295
304
</ Select >
296
305
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 >
309
327
</ div >
310
328
</ fieldset >
329
+ </ form >
330
+ ) ;
331
+ } ;
311
332
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 >
331
389
) }
332
- </ Form >
333
- ) ;
390
+ </ div >
391
+ ) ;
392
+ } ) ;
334
393
} ;
335
394
336
395
type TasksFilterProps = {
0 commit comments