feat: defer api key generation with mutation (#22318)

Closes #22065

This pull-request ensures that when we load the `<WorkspacePage />`
we're not instantly attempting to generate an `apiKey` every-time. These
are now only generated once the user attempts to actually click on the
VSCode link, this is now a mutation also (which is the correct action
for this).
This commit is contained in:
Jake Howell
2026-03-05 15:50:28 +11:00
committed by GitHub
parent 06105c9c62
commit f609de860f
+113 -40
View File
@@ -1,6 +1,6 @@
import Skeleton from "@mui/material/Skeleton";
import { API } from "api/api";
import { templateVersion } from "api/queries/templates";
import { apiKey } from "api/queries/users";
import {
cancelBuild,
deleteWorkspace,
@@ -629,9 +629,6 @@ type WorkspaceAppsProps = {
};
const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
const { data: apiKeyResponse } = useQuery(apiKey());
const token = apiKeyResponse?.key;
/**
* Coder is pretty flexible and allows an enormous variety of use cases, such
* as having multiple resources with many agents, but they are not common. The
@@ -665,39 +662,33 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
if (builtinApps.has("vscode")) {
buttons.push(
<BaseIconLink
<VSCodeIconLink
key="vscode"
isLoading={!token}
variant="vscode"
label="Open VSCode"
href={getVSCodeHref("vscode", {
owner: workspace.owner_name,
workspace: workspace.name,
agent: agent.name,
token: token ?? "",
folder: agent.expanded_directory,
})}
owner={workspace.owner_name}
workspace={workspace.name}
agent={agent.name}
folder={agent.expanded_directory}
>
<VSCodeIcon />
</BaseIconLink>,
</VSCodeIconLink>,
);
}
if (builtinApps.has("vscode_insiders")) {
buttons.push(
<BaseIconLink
<VSCodeIconLink
key="vscode-insiders"
variant="vscode-insiders"
label="Open VSCode Insiders"
isLoading={!token}
href={getVSCodeHref("vscode-insiders", {
owner: workspace.owner_name,
workspace: workspace.name,
agent: agent.name,
token: token ?? "",
folder: agent.expanded_directory,
})}
owner={workspace.owner_name}
workspace={workspace.name}
agent={agent.name}
folder={agent.expanded_directory}
>
<VSCodeInsidersIcon />
</BaseIconLink>,
</VSCodeIconLink>,
);
}
@@ -794,40 +785,122 @@ const IconAppLink: FC<IconAppLinkProps> = ({ app, workspace, agent }) => {
);
};
type BaseIconLinkProps = PropsWithChildren<{
type VSCodeIconLinkProps = PropsWithChildren<{
variant: "vscode" | "vscode-insiders";
label: string;
href: string;
isLoading?: boolean;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
target?: string;
owner: string;
workspace: string;
agent: string;
folder?: string;
}>;
// Generates an API key on click instead of on page load, since
// key generation is a POST request that should only fire when
// the user actually wants to open VS Code.
const VSCodeIconLink: FC<VSCodeIconLinkProps> = ({
variant,
label,
owner,
workspace,
agent,
folder,
children,
}) => {
const generateKeyMutation = useMutation({
mutationFn: () => API.getApiKey(),
onSuccess: ({ key }) => {
// We use a `location.href` here instead of a `navigate` because
// these are protocol-specific links.
location.href = getVSCodeHref(variant, {
owner,
workspace,
token: key,
agent,
folder,
});
},
});
return (
<BaseIconLink
label={label}
isLoading={generateKeyMutation.isPending}
onClick={() => {
if (!generateKeyMutation.isPending) {
generateKeyMutation.mutate();
}
}}
>
{children}
</BaseIconLink>
);
};
type BaseIconLinkCommonProps = PropsWithChildren<{
label: string;
isLoading?: boolean;
}>;
type BaseIconLinkAnchorProps = BaseIconLinkCommonProps & {
href: string;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
target?: string;
};
type BaseIconLinkButtonProps = BaseIconLinkCommonProps & {
href?: never;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
};
type BaseIconLinkProps = BaseIconLinkAnchorProps | BaseIconLinkButtonProps;
const BaseIconLink: FC<BaseIconLinkProps> = ({
href,
isLoading,
label,
children,
target,
onClick,
...rest
}) => {
const loadingClass = isLoading ? "animate-pulse" : "";
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon-lg" asChild>
<a
target={target}
className={isLoading ? "animate-pulse" : ""}
href={href}
{rest.href !== undefined ? (
<Button
variant="outline"
size="icon-lg"
asChild
disabled={isLoading}
>
<a
target={rest.target}
className={loadingClass}
href={rest.href}
onClick={(e) => {
e.stopPropagation();
rest.onClick?.(e);
}}
>
{children}
<span className="sr-only">{label}</span>
</a>
</Button>
) : (
<Button
variant="outline"
size="icon-lg"
className={loadingClass}
onClick={(e) => {
e.stopPropagation();
onClick?.(e);
rest.onClick(e);
}}
disabled={isLoading}
>
{children}
<span className="sr-only">{label}</span>
</a>
</Button>
</Button>
)}
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>