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)
This commit is contained in:
Bruno Quaresma
2025-06-27 10:44:39 -03:00
committed by GitHub
parent 2d44add81f
commit 59a65415b4
3 changed files with 117 additions and 57 deletions
+1
View File
@@ -50,5 +50,6 @@ export const useExternalAuth = (versionId: string | undefined) => {
externalAuthPollingState,
isLoadingExternalAuth,
externalAuthError: error,
isPollingExternalAuth: externalAuthPollingState === "polling",
};
};
@@ -245,7 +245,7 @@ export const MissingExternalAuth: Story = {
});
await step("Renders external authentication", async () => {
await canvas.findByRole("button", { name: /login with github/i });
await canvas.findByRole("button", { name: /connect to github/i });
});
},
};
+115 -56
View File
@@ -2,13 +2,12 @@ import Skeleton from "@mui/material/Skeleton";
import { API } from "api/api";
import { getErrorDetail, getErrorMessage } from "api/errors";
import { disabledRefetchOptions } from "api/queries/util";
import type { Template } from "api/typesGenerated";
import type { Template, TemplateVersionExternalAuth } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
import { Button } from "components/Button/Button";
import { Form, FormFields, FormSection } from "components/Form/Form";
import { displayError } from "components/GlobalSnackbar/utils";
import { Margins } from "components/Margins/Margins";
import {
@@ -37,9 +36,16 @@ import {
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useAuthenticated } from "hooks";
import { useExternalAuth } from "hooks/useExternalAuth";
import { RotateCcwIcon, SendIcon } from "lucide-react";
import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react";
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
@@ -50,12 +56,12 @@ import { Link as RouterLink } from "react-router-dom";
import TextareaAutosize from "react-textarea-autosize";
import { pageTitle } from "utils/page";
import { relativeTime } from "utils/time";
import { ExternalAuthButton } from "../CreateWorkspacePage/ExternalAuthButton";
import { type UserOption, UsersCombobox } from "./UsersCombobox";
type TasksFilter = {
user: UserOption | undefined;
};
const TasksPage: FC = () => {
const { user, permissions } = useAuthenticated();
const [filter, setFilter] = useState<TasksFilter>({
@@ -201,21 +207,24 @@ type TaskFormProps = {
const TaskForm: FC<TaskFormProps> = ({ templates }) => {
const { user } = useAuthenticated();
const queryClient = useQueryClient();
const [templateId, setTemplateId] = useState<string>(templates[0].id);
const [selectedTemplateId, setSelectedTemplateId] = useState<string>(
templates[0].id,
);
const selectedTemplate = templates.find(
(t) => t.id === selectedTemplateId,
) as Template;
const {
externalAuth,
externalAuthPollingState,
startPollingExternalAuth,
isLoadingExternalAuth,
externalAuthError,
} = useExternalAuth(
templates.find((t) => t.id === templateId)?.active_version_id,
);
const hasAllRequiredExternalAuth = externalAuth?.every(
(auth) => auth.optional || auth.authenticated,
isPollingExternalAuth,
isLoadingExternalAuth,
} = useExternalAuth(selectedTemplate.active_version_id);
const missedExternalAuth = externalAuth?.filter(
(auth) => !auth.optional && !auth.authenticated,
);
const isMissingExternalAuth = missedExternalAuth
? missedExternalAuth.length > 0
: true;
const createTaskMutation = useMutation({
mutationFn: async ({ prompt, templateId }: CreateTaskMutationFnProps) =>
@@ -235,10 +244,6 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
const prompt = formData.get("prompt") as string;
const templateID = formData.get("templateID") as string;
if (!prompt || !templateID) {
return;
}
try {
await createTaskMutation.mutateAsync({
prompt,
@@ -253,8 +258,12 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
};
return (
<Form onSubmit={onSubmit} aria-label="Create AI task">
{Boolean(externalAuthError) && <ErrorAlert error={externalAuthError} />}
<form
onSubmit={onSubmit}
aria-label="Create AI task"
className="flex flex-col gap-4"
>
{externalAuthError && <ErrorAlert error={externalAuthError} />}
<fieldset
className="border border-border border-solid rounded-lg p-4"
@@ -274,7 +283,7 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
<div className="flex items-center justify-between pt-2">
<Select
name="templateID"
onValueChange={(value) => setTemplateId(value)}
onValueChange={(value) => setSelectedTemplateId(value)}
defaultValue={templates[0].id}
required
>
@@ -294,45 +303,95 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
</SelectContent>
</Select>
<Button
size="sm"
type="submit"
disabled={!hasAllRequiredExternalAuth}
>
<Spinner
loading={createTaskMutation.isPending || isLoadingExternalAuth}
>
<SendIcon />
</Spinner>
Run task
</Button>
<div className="flex items-center gap-2">
{missedExternalAuth && (
<ExternalAuthButtons
template={selectedTemplate}
missedExternalAuth={missedExternalAuth}
/>
)}
<Button size="sm" type="submit" disabled={isMissingExternalAuth}>
<Spinner
loading={
isLoadingExternalAuth ||
isPollingExternalAuth ||
createTaskMutation.isPending
}
>
<SendIcon />
</Spinner>
Run task
</Button>
</div>
</div>
</fieldset>
{!hasAllRequiredExternalAuth &&
externalAuth &&
externalAuth.length > 0 && (
<FormSection
title="External Authentication"
description="This template uses external services for authentication."
>
<FormFields>
{externalAuth.map((auth) => (
<ExternalAuthButton
key={auth.id}
auth={auth}
isLoading={externalAuthPollingState === "polling"}
onStartPolling={startPollingExternalAuth}
displayRetry={externalAuthPollingState === "abandoned"}
/>
))}
</FormFields>
</FormSection>
)}
</Form>
</form>
);
};
type ExternalAuthButtonProps = {
template: Template;
missedExternalAuth: TemplateVersionExternalAuth[];
};
const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
template,
missedExternalAuth,
}) => {
const {
startPollingExternalAuth,
isPollingExternalAuth,
externalAuthPollingState,
} = useExternalAuth(template.active_version_id);
const shouldRetry = externalAuthPollingState === "abandoned";
return missedExternalAuth.map((auth) => {
return (
<div className="flex items-center gap-2" key={auth.id}>
<Button
variant="outline"
size="sm"
disabled={isPollingExternalAuth || auth.authenticated}
onClick={() => {
window.open(
auth.authenticate_url,
"_blank",
"width=900,height=600",
);
startPollingExternalAuth();
}}
>
<Spinner loading={isPollingExternalAuth}>
<ExternalImage src={auth.display_icon} />
</Spinner>
Connect to {auth.display_name}
</Button>
{shouldRetry && !auth.authenticated && (
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={startPollingExternalAuth}
>
<RedoIcon />
<span className="sr-only">Refresh external auth</span>
</Button>
</TooltipTrigger>
<TooltipContent>
Retry connecting to {auth.display_name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
);
});
};
type TasksFilterProps = {
filter: TasksFilter;
onFilterChange: (filter: TasksFilter) => void;