diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 03ffc4781c9..34766f1166f 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -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 = ({ 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 = ({ workspace }) => { if (builtinApps.has("vscode")) { buttons.push( - - , + , ); } if (builtinApps.has("vscode_insiders")) { buttons.push( - - , + , ); } @@ -794,40 +785,122 @@ const IconAppLink: FC = ({ app, workspace, agent }) => { ); }; -type BaseIconLinkProps = PropsWithChildren<{ +type VSCodeIconLinkProps = PropsWithChildren<{ + variant: "vscode" | "vscode-insiders"; + label: 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 = ({ + 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 ( + { + if (!generateKeyMutation.isPending) { + generateKeyMutation.mutate(); + } + }} + > + {children} + + ); +}; + +type BaseIconLinkCommonProps = PropsWithChildren<{ label: string; - href: string; isLoading?: boolean; +}>; + +type BaseIconLinkAnchorProps = BaseIconLinkCommonProps & { + href: string; onClick?: (e: React.MouseEvent) => void; target?: string; -}>; +}; + +type BaseIconLinkButtonProps = BaseIconLinkCommonProps & { + href?: never; + onClick: (e: React.MouseEvent) => void; +}; + +type BaseIconLinkProps = BaseIconLinkAnchorProps | BaseIconLinkButtonProps; const BaseIconLink: FC = ({ - href, isLoading, label, children, - target, - onClick, + ...rest }) => { + const loadingClass = isLoading ? "animate-pulse" : ""; + return ( - + ) : ( + + + )} {label}