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

Skip to content

refactor: poll for git auth updates when creating a workspace #9804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 26, 2023
Prev Previous commit
Next Next commit
refactor: poll for git auth updates when connecting a provider
  • Loading branch information
aslilac committed Sep 21, 2023
commit ed72bf52dbd35e9a87a7fdc86716ec1c70648f98
13 changes: 13 additions & 0 deletions site/src/api/queries/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,16 @@ export const updateActiveTemplateVersion = (
},
};
};

export const templateVersionGitAuthKey = (versionId: string) => [
"templateVersion",
versionId,
"gitAuth",
];

export const templateVersionGitAuth = (versionId: string) => {
return {
queryKey: templateVersionGitAuthKey(versionId),
queryFn: () => API.getTemplateVersionGitAuth(versionId),
};
};
63 changes: 47 additions & 16 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { useMachine } from "@xstate/react";
import {
Template,
TemplateVersionGitAuth,
TemplateVersionParameter,
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { useMe } from "hooks/useMe";
import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react";
import { type FC, useCallback, useState, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { pageTitle } from "utils/page";
Expand All @@ -25,6 +23,10 @@ import {
colors,
NumberDictionary,
} from "unique-names-generator";
import { useQuery } from "@tanstack/react-query";
import { templateVersionGitAuth } from "api/queries/templates";

export type GitAuthPollingState = "idle" | "polling" | "abandoned";

const CreateWorkspacePage: FC = () => {
const organizationId = useOrganizationId();
Expand All @@ -50,18 +52,45 @@ const CreateWorkspacePage: FC = () => {
},
},
});
const {
template,
error,
parameters,
permissions,
gitAuth,
defaultName,
versionId,
} = createWorkspaceState.context;
const { template, parameters, permissions, defaultName, versionId } =
createWorkspaceState.context;
const title = createWorkspaceState.matches("autoCreating")
? "Creating workspace..."
: "Create Workspace";
: "Create workspace";

const [gitAuthPollingState, setGitAuthPollingState] =
useState<GitAuthPollingState>("idle");

useEffect(() => {
if (gitAuthPollingState !== "polling") {
return;
}

// Poll for a maximum of one minute
const quitPolling = setTimeout(
() => setGitAuthPollingState("abandoned"),
60_000,
);
return () => {
clearTimeout(quitPolling);
};
}, [gitAuthPollingState]);

const startPollingGitAuth = useCallback(() => {
if (gitAuthPollingState === "polling") {
return;
}
setGitAuthPollingState("polling");
}, [gitAuthPollingState]);

const { data: gitAuth, error } = useQuery(
versionId
? {
...templateVersionGitAuth(versionId),
refetchInterval: gitAuthPollingState === "polling" ? 1000 : false,
}
: { enabled: false },
);

return (
<>
Expand All @@ -81,11 +110,13 @@ const CreateWorkspacePage: FC = () => {
defaultOwner={me}
defaultBuildParameters={defaultBuildParameters}
error={error}
template={template as Template}
template={template!}
versionId={versionId}
gitAuth={gitAuth as TemplateVersionGitAuth[]}
gitAuth={gitAuth ?? []}
gitAuthPollingState={gitAuthPollingState}
startPollingGitAuth={startPollingGitAuth}
permissions={permissions as CreateWSPermissions}
parameters={parameters as TemplateVersionParameter[]}
parameters={parameters!}
creatingWorkspace={createWorkspaceState.matches("creatingWorkspace")}
onCancel={() => {
navigate(-1);
Expand Down
13 changes: 10 additions & 3 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { CreateWSPermissions } from "xServices/createWorkspace/createWorkspaceXS
import { GitAuth } from "./GitAuth";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Stack } from "components/Stack/Stack";
import { type GitAuthPollingState } from "./CreateWorkspacePage";

export interface CreateWorkspacePageViewProps {
error: unknown;
Expand All @@ -38,6 +39,8 @@ export interface CreateWorkspacePageViewProps {
template: TypesGen.Template;
versionId?: string;
gitAuth: TypesGen.TemplateVersionGitAuth[];
gitAuthPollingState: GitAuthPollingState;
startPollingGitAuth: () => void;
parameters: TypesGen.TemplateVersionParameter[];
defaultBuildParameters: TypesGen.WorkspaceBuildParameter[];
permissions: CreateWSPermissions;
Expand All @@ -56,6 +59,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
template,
versionId,
gitAuth,
gitAuthPollingState,
startPollingGitAuth,
parameters,
defaultBuildParameters,
permissions,
Expand Down Expand Up @@ -113,7 +118,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
>
<FormFields>
<SelectedTemplate template={template} />
{versionId && (
{versionId !== template.active_version_id && (
<Stack spacing={1} className={styles.hasDescription}>
<TextField
disabled
Expand Down Expand Up @@ -161,11 +166,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
description="This template requires authentication to automatically perform Git operations on create."
>
<FormFields>
{gitAuth.map((auth, index) => (
{gitAuth.map((auth) => (
<GitAuth
key={index}
key={auth.id}
authenticateURL={auth.authenticate_url}
authenticated={auth.authenticated}
gitAuthPollingState={gitAuthPollingState}
startPollingGitAuth={startPollingGitAuth}
type={auth.type}
error={gitAuthErrors[auth.id]}
/>
Expand Down
45 changes: 30 additions & 15 deletions site/src/pages/CreateWorkspacePage/GitAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,30 @@ import { BitbucketIcon } from "components/Icons/BitbucketIcon";
import { GitlabIcon } from "components/Icons/GitlabIcon";
import { FC } from "react";
import { makeStyles } from "@mui/styles";
import { type GitAuthPollingState } from "./CreateWorkspacePage";
import { Stack } from "components/Stack/Stack";
import ReplayIcon from "@mui/icons-material/Replay";
import { LoadingButton } from "components/LoadingButton/LoadingButton";

export interface GitAuthProps {
type: TypesGen.GitProvider;
authenticated: boolean;
authenticateURL: string;
gitAuthPollingState: GitAuthPollingState;
startPollingGitAuth: () => void;
error?: string;
}

export const GitAuth: FC<GitAuthProps> = ({
type,
authenticated,
authenticateURL,
error,
}) => {
export const GitAuth: FC<GitAuthProps> = (props) => {
const {
type,
authenticated,
authenticateURL,
gitAuthPollingState,
startPollingGitAuth,
error,
} = props;

const styles = useStyles({
error: typeof error !== "undefined",
});
Expand Down Expand Up @@ -52,12 +62,11 @@ export const GitAuth: FC<GitAuthProps> = ({

return (
<Tooltip
title={
authenticated ? "You're already authenticated! No action needed." : ``
}
title={authenticated && `${prettyName} has already been connected.`}
>
<div>
<Button
<Stack alignItems="center" spacing={1}>
<LoadingButton
loading={gitAuthPollingState === "polling"}
href={authenticateURL}
variant="contained"
size="large"
Expand All @@ -73,15 +82,21 @@ export const GitAuth: FC<GitAuthProps> = ({
return;
}
window.open(authenticateURL, "_blank", "width=900,height=600");
startPollingGitAuth();
}}
>
{authenticated
? `You're authenticated with ${prettyName}!`
: `Click to login with ${prettyName}!`}
</Button>
? `Authenticated with ${prettyName}`
: `Login with ${prettyName}`}
</LoadingButton>

{gitAuthPollingState === "abandoned" && (
<Button variant="text" onClick={startPollingGitAuth}>
<ReplayIcon /> Check again
</Button>
)}
{error && <FormHelperText error>{error}</FormHelperText>}
</div>
</Stack>
</Tooltip>
);
};
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/GitAuthPage/GitAuthPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const GitAuthPageView: FC<GitAuthPageViewProps> = ({
<SignInLayout>
<Welcome message={`You've authenticated with ${gitAuth.type}!`} />
<p className={styles.text}>
Hey @{gitAuth.user?.login} 👋!{" "}
Hey @{gitAuth.user?.login}! 👋{" "}
{(!gitAuth.app_installable || gitAuth.installations.length > 0) &&
"You are now authenticated with Git. Feel free to close this window!"}
</p>
Expand Down
33 changes: 8 additions & 25 deletions site/src/xServices/createWorkspace/createWorkspaceXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ type CreateWorkspaceContext = {
template?: Template;
parameters?: TemplateVersionParameter[];
permissions?: Record<string, boolean>;
gitAuth?: TemplateVersionGitAuth[];
// Used on auto-create
defaultBuildParameters?: WorkspaceBuildParameter[];
};
Expand All @@ -49,7 +48,7 @@ type RefreshGitAuthEvent = {
};

export const createWorkspaceMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QGMBOYCGAXMB1A9qgNawAOGyYAyltmAHTIAWYyRAlgHZQCy+EYAMQBtAAwBdRKFL5Y7LO3ycpIAB6IATAGYN9UQBYArDsP6A7Pq1WNAGhABPRAE4AHPUOHRG7xoCMxswA2Mw0AX1C7NEwcAmIyCmpaHEYWNi5efiFhX0kkEBk5BSUVdQQ-M3onM19fUV8nfRdvJ0M7RzKdejMPVw0mpyctJ0DwyPQ6WJJySho6egwAVyx8AGFxhW5BCCUGLgA3fCIGKInCKYTZ5MXltej0hH38ZGxFTjFxd5UC+VeSxA8KlULIN9AYXC4zE42ogALT1eiBLSGbyBQIQvoufSuUYgE4xM7xGZJBjXVbrdKCMCoVCEeikAA22AAZoQALaMdZ4AnTRJzUm3F7cB6cA7PIpvCSfPLfcV-BBaFxOegKwzVRouUTdAxmaEIXwuQx6QyBPq+QKgg2iEYRXGcyaE3nJen4DAQdIAMTZABFsBgtjt6I8jhzoly4jzLgxna6Pd7fcLRS8lO8pdJZD9inlSsEtF0qvoNI0rUEBrqtJZ6BpAtqFaIvE4cXiw+ciXNo27uJ7UKyfbRKdTaQzmWyQ6dwxdifR27Hu72MAmnkmJR8JF907Ks4hFYEjZiLKJDL4jIWyxWqwZ9Poj74LBDG3buRO5uwIPShCsAEoAUQAggAVL8AH1cAAeQ-ABpKgAAUfxWL9U3yddfk3BBAkMJVFScOtvExcwml1VVDS8dVAkGFx6mNe9Q3tCNJzxdIaISf1OF2EVDmOB9x1bZJ6O4RjKAXMVXhTVdpSQzNQGzRF3F8bxLAPJxCw0VoHEQSFRARKpyyMJEWiMKixxbR0OLuPjH0ofsaVQOlGSwFlu1HfEuOMxyGPMsBBKXETcjTQpkMkxAtGrSpyytFxAjNPxvAI0RcycXwtE1boISw9DDHCG1OEyeA8ibfjjLXPyJLUWEEq6Uj9DRZSNAPdCIV1OFNXoBLkQ0QZzUxUQGxtPL3MjFJWA4bg+AEQqM2UFDy0rRLjBvcwGlBXxdWGZVsKCtry1MYwDKcoz+v5cluDGjcAvlRSER0MwrqPcKEqhVSEBCegNS8LQ5pmwidubB1+unTs41oY7-JK1DgtNDVPCutL9F1bQ3EU0ErSqiH0p6zi9snF83yB4rswhPRUWGIJ9SvFpdSMXxmshfxIvQxEwjR6i+row6oHynGJtO80lQGVFgnIsxwS8XVzR3bpC2GaszXMXwvvy-qmwgDm5VIndvCsOtIUqmpbAexVdESiwC1VJEEv0OXmbbF0IC-AdUGVlD4t0KsPHe+o6h1B6jA0zVYuNPotCCSEMtCIA */
/** @xstate-layout N4IgpgJg5mDOIC5QGMBOYCGAXMB1A9qgNawAOGyYAyltmAHTIAWYyRAlgHZQCy+EYAMQBtAAwBdRKFL5Y7LO3ycpIAB6IATABYAnPR2iDWgOwBmAGynRo4wFZbAGhABPRDoAc9d+8OnTARlF-Uy0NUQ0AXwinNEwcAmIyCmpaHEYWNi5efiFhf0kkEBk5BSUVdQQNU1svEPcq9wsDU3ctJ1dKv3pRd3NzHVNjf1sjHX8omPQ6BJJySho6egwAVyx8AGEphW5BCCUGLgA3fCIGWOnCWeSFtJW1zbishCP8ZGxFTjFxL5Vi+Q-yoh7MZ9MZjLoQqItN5jDp2ogALT+PSWWwaDR9dzGDTeXTuCYgc7xS5JeapBh3DZbLKCMCoVCEeikAA22AAZoQALaMLZ4ElzFKLSkPd7cZ6cY5vUqfCQ-Qp-aWAhAtPQtWxDaE+OxQ4zwhD+dw1US2cw4-zmLQ9WyicwEol8xICm4MZn4DAQLIAMS5ABFsBhdvt6C9Tjy4g6rmTFq73V7ff7xZL3kovnLpLJ-mVChVzNb6P5tDoMei7P5-G0XIgQqZ6BjbFrWuZGrC7byZqTBWkYx7uN7UJy-bRafTGSz2VywxdHddyfRu3H+4OMInXsmZd8JL8M4rs4hejWCzYLOYDSfjOY9SENPpgmYy+bgjodLbooS2-yZ4t2BBmUJ1gAlABRABBAAVQCAH1cAAeX-ABpKgAAVgPWQC0yKbcAV3BBLHMWs61EQZjQ0ZF3D1ewa36S0QlhasQlbcN2ydWciSyJjkkDTgDglE4znfacozSVjuHYygVylD5U03eVMKzUAc1MPRGh0cEtBMJ9Wj1MxPFsKwmg0DwwmGBip0jTs+MeESP0oYcGVQJlWSwDl+0nYkBPM1y2OssBxLXKSCnTEosPkxBQn8eh7FaewBjRU1THIwIb2CLQ+nrXN1SiV9OByeBCntUTzK3IK5LUKstFrWE4uNGxwQxPUkRsfNqnRfxzybE1+hMtyzOddJWA4bg+AEIrM2UbD6gq58qmqsFQgvSsEGfehLEMExWp0LRSK6iMO164VqW4EadxC5Ui2W0wNDBDVekfLTrx8cIAnBKxgVsbaCt6+de3jWgjuC0qcIsZbfBtPE-F1BaGn0AzIWxGK-HxV98u83rv1-P6SpzSx6EUtT+kIsYT38PUtFscKDTUw1zGxMmy3elGWIOqACoxsaTtNPCLs2-oGlNVq9SJ2tQcCS0eZS+n3N6+0IFZpV3A2-MtBadx1uRcIyIWuxPDsforEM6x7AlnrZ27QCR1QWXxthHHLrBJ8nxtJsSd0ehsQsJWNHrYYi0yiIgA */
createMachine(
{
id: "createWorkspaceState",
Expand All @@ -64,7 +63,6 @@ export const createWorkspaceMachine =
template: Template;
permissions: CreateWSPermissions;
parameters: TemplateVersionParameter[];
gitAuth: TemplateVersionGitAuth[];
};
};
createWorkspace: {
Expand Down Expand Up @@ -112,16 +110,6 @@ export const createWorkspaceMachine =
},
},
idle: {
invoke: [
{
src: () => (callback) => {
const channel = watchGitAuthRefresh(() => {
callback("REFRESH_GITAUTH");
});
return () => channel.close();
},
},
],
on: {
CREATE_WORKSPACE: {
target: "creatingWorkspace",
Expand Down Expand Up @@ -189,23 +177,24 @@ export const createWorkspaceMachine =
rich_parameter_values: defaultBuildParameters,
});
},
loadFormData: async ({ templateName, organizationId }) => {
loadFormData: async ({ templateName, organizationId, versionId }) => {
const [template, permissions] = await Promise.all([
getTemplateByName(organizationId, templateName),
checkCreateWSPermissions(organizationId),
]);
const [parameters, gitAuth] = await Promise.all([
getTemplateVersionRichParameters(template.active_version_id).then(
(p) => p.filter(paramsUsedToCreateWorkspace),

const realizedVersionId = versionId ?? template.active_version_id;
const [parameters] = await Promise.all([
getTemplateVersionRichParameters(realizedVersionId).then((p) =>
p.filter(paramsUsedToCreateWorkspace),
),
getTemplateVersionGitAuth(template.active_version_id),
]);

return {
template,
permissions,
parameters,
gitAuth,
versionId: realizedVersionId,
};
},
},
Expand Down Expand Up @@ -246,12 +235,6 @@ const checkCreateWSPermissions = async (organizationId: string) => {
}) as Promise<Record<keyof typeof permissionsToCheck, boolean>>;
};

export const watchGitAuthRefresh = (callback: () => void) => {
const bc = new BroadcastChannel(REFRESH_GITAUTH_BROADCAST_CHANNEL);
bc.addEventListener("message", callback);
return bc;
};
Comment on lines -249 to -253
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused... this 100% was working, it's in fact how it worked until this entire time.

This removes the need to poll, and generally, we should avoid idle polling on pages. If users leave the "Create Workspace" page up, I don't think it's fair that an operator needs to consume more API requests.

This was a proper regression if it stopped working, not just existing code that didn't work!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little different than that case, because it has a timeout where it will stop polling until the user interacts again, so this definitely won't be an "increases load by xx%" type of thing. I get the appeal of BroadcastChannel, but I've always found that it ends up being fragile for stuff like this.


export type CreateWSPermissions = Awaited<
ReturnType<typeof checkCreateWSPermissions>
>;