Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions site/src/hooks/useExternalAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,75 @@ import { templateVersionExternalAuth } from "#/api/queries/templates";
export type ExternalAuthPollingState = "idle" | "polling" | "abandoned";

export const useExternalAuth = (versionId: string | undefined) => {
const [externalAuthPollingState, setExternalAuthPollingState] =
useState<ExternalAuthPollingState>("idle");
const [pollingState, setPollingState] = useState<
Record<string, ExternalAuthPollingState>
>({});

const startPollingExternalAuth = useCallback(() => {
setExternalAuthPollingState("polling");
const startPollingExternalAuth = useCallback((providerId: string) => {
setPollingState((prev) => ({ ...prev, [providerId]: "polling" }));
}, []);

const isAnyPolling = Object.values(pollingState).some((s) => s === "polling");

const {
data: externalAuth,
isPending: isLoadingExternalAuth,
error,
} = useQuery({
...templateVersionExternalAuth(versionId ?? ""),
enabled: Boolean(versionId),
refetchInterval: externalAuthPollingState === "polling" ? 1000 : false,
refetchInterval: isAnyPolling ? 1000 : false,
});

const allSignedIn = externalAuth?.every((it) => it.authenticated);

// Stop polling individual providers once they authenticate.
useEffect(() => {
if (allSignedIn) {
setExternalAuthPollingState("idle");
if (!externalAuth) {
return;
}
setPollingState((prev) => {
let changed = false;
const next = { ...prev };
for (const auth of externalAuth) {
if (auth.authenticated && next[auth.id] === "polling") {
next[auth.id] = "idle";
changed = true;
}
}
return changed ? next : prev;
});
}, [externalAuth]);

if (externalAuthPollingState !== "polling") {
// Per-provider 60-second timeout.
useEffect(() => {
const pollingIds = Object.entries(pollingState)
.filter(([, authPollingState]) => authPollingState === "polling")
.map(([id]) => id);

if (pollingIds.length === 0) {
return;
}

// Poll for a maximum of one minute
const quitPolling = setTimeout(
() => setExternalAuthPollingState("abandoned"),
60_000,
const timers = pollingIds.map((id) =>
setTimeout(() => {
setPollingState((prev) =>
prev[id] === "polling" ? { ...prev, [id]: "abandoned" } : prev,
);
}, 60_000),
);

return () => {
clearTimeout(quitPolling);
for (const t of timers) {
clearTimeout(t);
}
};
}, [externalAuthPollingState, allSignedIn]);
}, [pollingState]);

return {
startPollingExternalAuth,
externalAuth,
externalAuthPollingState,
externalAuthPollingState: pollingState,
isLoadingExternalAuth,
externalAuthError: error,
isPollingExternalAuth: externalAuthPollingState === "polling",
isPollingExternalAuth: isAnyPolling,
};
};
34 changes: 34 additions & 0 deletions site/src/modules/tasks/TaskPrompt/TaskPrompt.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MockTasks,
MockTemplate,
MockTemplateVersion,
MockTemplateVersionExternalAuthAzure,
MockTemplateVersionExternalAuthGithub,
MockTemplateVersionExternalAuthGithubAuthenticated,
MockUserOwner,
Expand Down Expand Up @@ -387,6 +388,39 @@ export const MissingExternalAuth: Story = {
},
};

export const MissingExternalAuthMultipleProviders: Story = {
beforeEach: () => {
spyOn(API, "getTasks")
.mockResolvedValueOnce(MockTasks)
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
spyOn(API, "createTask").mockResolvedValue(MockTask);
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
MockTemplateVersionExternalAuthGithub,
MockTemplateVersionExternalAuthAzure,
]);
// Prevent the auth button from actually opening a popup.
spyOn(window, "open").mockReturnValue(null);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

const githubButton = await canvas.findByRole("button", {
name: /connect to github/i,
});
const azureButton = await canvas.findByRole("button", {
name: /connect to azure/i,
});

await step("Click GitHub auth button", async () => {
await userEvent.click(githubButton);
});

await step("Azure button remains enabled", () => {
expect(azureButton).toBeEnabled();
});
},
};

export const ExternalAuthError: Story = {
beforeEach: () => {
spyOn(API, "getTasks")
Expand Down
16 changes: 8 additions & 8 deletions site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,14 +436,14 @@ const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
versionId,
missedExternalAuth,
}) => {
const {
startPollingExternalAuth,
isPollingExternalAuth,
externalAuthPollingState,
} = useExternalAuth(versionId);
const shouldRetry = externalAuthPollingState === "abandoned";
const { startPollingExternalAuth, externalAuthPollingState } =
useExternalAuth(versionId);

return missedExternalAuth.map((auth) => {
const isPollingExternalAuth =
externalAuthPollingState[auth.id] === "polling";
const shouldRetry = externalAuthPollingState[auth.id] === "abandoned";

return (
<div className="flex items-center gap-2" key={auth.id}>
<Button
Expand All @@ -456,7 +456,7 @@ const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
"_blank",
"width=900,height=600",
);
startPollingExternalAuth();
startPollingExternalAuth(auth.id);
}}
>
<Spinner loading={isPollingExternalAuth}>
Expand All @@ -471,7 +471,7 @@ const ExternalAuthButtons: FC<ExternalAuthButtonProps> = ({
<Button
variant="outline"
size="icon"
onClick={startPollingExternalAuth}
onClick={() => startPollingExternalAuth(auth.id)}
>
<RedoIcon />
<span className="sr-only">Refresh external auth</span>
Expand Down
Loading
Loading