diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts index 98049a89e7ffe..d0d8557683792 100644 --- a/site/src/api/queries/workspaceBuilds.ts +++ b/site/src/api/queries/workspaceBuilds.ts @@ -1,7 +1,18 @@ -import { UseInfiniteQueryOptions } from "react-query"; +import { QueryOptions, UseInfiniteQueryOptions } from "react-query"; import * as API from "api/api"; import { WorkspaceBuild, WorkspaceBuildsRequest } from "api/typesGenerated"; +export function workspaceBuildParametersKey(workspaceId: string) { + return ["workspaceBuilds", workspaceId, "parameters"] as const; +} + +export function workspaceBuildParameters(workspaceBuildId: string) { + return { + queryKey: workspaceBuildParametersKey(workspaceBuildId), + queryFn: () => API.getWorkspaceBuildParameters(workspaceBuildId), + } as const satisfies QueryOptions; +} + export const workspaceBuildByNumber = ( username: string, workspaceName: string, diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index bd5b76f951838..4607464621e2d 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -26,7 +26,7 @@ export const workspaceByOwnerAndName = ( }; }; -type AutoCreateWorkspaceOptions = { +export type AutoCreateWorkspaceOptions = { templateName: string; versionId?: string; organizationId: string; @@ -84,6 +84,27 @@ export const autoCreateWorkspace = (queryClient: QueryClient) => { }; }; +export function workspaceParametersKey(workspaceId: string) { + return ["workspaces", workspaceId, "parameters"] as const; +} + +export function workspaceParameters(workspace: Workspace) { + return { + queryKey: workspaceParametersKey(workspace.id), + queryFn: () => API.getWorkspaceParameters(workspace), + } as const; +} + +export function updateWorkspaceParameters(workspaceId: string) { + return { + mutationFn: (buildParameters: WorkspaceBuildParameter[]) => + API.postWorkspaceBuild(workspaceId, { + transition: "start", + rich_parameter_values: buildParameters, + }), + } as const; +} + export function workspacesKey(config: WorkspacesRequest = {}) { const { q, limit } = config; return ["workspaces", { q, limit }] as const; diff --git a/site/src/components/TemplateParameters/TemplateParameters.tsx b/site/src/components/TemplateParameters/TemplateParameters.tsx index 9befc7c74051f..2213166b3ae71 100644 --- a/site/src/components/TemplateParameters/TemplateParameters.tsx +++ b/site/src/components/TemplateParameters/TemplateParameters.tsx @@ -17,68 +17,60 @@ export type TemplateParametersSectionProps = { export const MutableTemplateParametersSection: FC< TemplateParametersSectionProps > = ({ templateParameters, getInputProps, ...formSectionProps }) => { - const hasMutableParameters = - templateParameters.filter((p) => p.mutable).length > 0; + const mutableParameters = templateParameters.filter((p) => p.mutable); + + if (mutableParameters.length === 0) { + return null; + } return ( - <> - {hasMutableParameters && ( - - - {templateParameters.map( - (parameter, index) => - parameter.mutable && ( - - ), - )} - - - )} - + + + {mutableParameters.map((parameter, index) => ( + + ))} + + ); }; export const ImmutableTemplateParametersSection: FC< TemplateParametersSectionProps > = ({ templateParameters, getInputProps, ...formSectionProps }) => { - const hasImmutableParameters = - templateParameters.filter((p) => !p.mutable).length > 0; + const immutableParams = templateParameters.filter((p) => !p.mutable); + + if (immutableParams.length === 0) { + return null; + } return ( - <> - {hasImmutableParameters && ( - - These settings cannot be changed after creating - the workspace. - - } - > - - {templateParameters.map( - (parameter, index) => - !parameter.mutable && ( - - ), - )} - - - )} - + + These settings cannot be changed after creating the + workspace. + + } + > + + {immutableParams.map((parameter, index) => ( + + ))} + + ); }; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx index 2c6cff4807e81..045efa021fc39 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx @@ -24,125 +24,111 @@ import { templateVersionExternalAuth, richParameters, } from "api/queries/templates"; -import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; +import { + AutoCreateWorkspaceOptions, + autoCreateWorkspace, + createWorkspace, +} from "api/queries/workspaces"; import { checkAuthorization } from "api/queries/authCheck"; import { CreateWSPermissions, createWorkspaceChecks } from "./permissions"; -import { paramsUsedToCreateWorkspace } from "utils/workspace"; import { useEffectEvent } from "hooks/hookPolyfills"; +import { workspaceBuildParameters } from "api/queries/workspaceBuilds"; -type CreateWorkspaceMode = "form" | "auto"; +export const createWorkspaceModes = ["form", "auto", "duplicate"] as const; +export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number]; -export type ExternalAuthPollingState = "idle" | "polling" | "abandoned"; +export type ExternalAuthPollingStatus = "idle" | "polling" | "abandoned"; const CreateWorkspacePage: FC = () => { const organizationId = useOrganizationId(); + const [searchParams] = useSearchParams(); const { template: templateName } = useParams() as { template: string }; - const me = useMe(); const navigate = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); - const defaultBuildParameters = getDefaultBuildParameters(searchParams); - const mode = (searchParams.get("mode") ?? "form") as CreateWorkspaceMode; - const customVersionId = searchParams.get("version") ?? undefined; - const defaultName = - mode === "auto" ? generateUniqueName() : searchParams.get("name") ?? ""; + const me = useMe(); const queryClient = useQueryClient(); - const autoCreateWorkspaceMutation = useMutation( - autoCreateWorkspace(queryClient), - ); const createWorkspaceMutation = useMutation(createWorkspace(queryClient)); const templateQuery = useQuery(templateByName(organizationId, templateName)); const permissionsQuery = useQuery( - checkAuthorization({ - checks: createWorkspaceChecks(organizationId), - }), + checkAuthorization({ checks: createWorkspaceChecks(organizationId) }), ); - const realizedVersionId = - customVersionId ?? templateQuery.data?.active_version_id; + + const versionId = + searchParams.get("version") ?? templateQuery.data?.active_version_id; + + const { authList, pollingStatus, startPolling } = useExternalAuth(versionId); const richParametersQuery = useQuery({ - ...richParameters(realizedVersionId ?? ""), - enabled: realizedVersionId !== undefined, + ...richParameters(versionId ?? ""), + enabled: versionId !== undefined, }); - const realizedParameters = richParametersQuery.data - ? richParametersQuery.data.filter(paramsUsedToCreateWorkspace) - : undefined; - const { externalAuth, externalAuthPollingState, startPollingExternalAuth } = - useExternalAuth(realizedVersionId); + const defaultBuildParameters = getDefaultBuildParameters(searchParams); + const mode = getWorkspaceMode(searchParams); + const defaultName = getDefaultName(mode, searchParams); + + const onCreateWorkspace = (workspace: Workspace) => { + navigate(`/@${workspace.owner_name}/${workspace.name}`); + }; + + const isAutoCreating = useAutomatedWorkspaceCreation({ + auto: mode === "auto", + onSuccess: onCreateWorkspace, + payload: { + templateName, + organizationId, + defaultBuildParameters, + defaultName, + versionId, + }, + }); const isLoadingFormData = templateQuery.isLoading || permissionsQuery.isLoading || richParametersQuery.isLoading; + const loadFormDataError = templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error; - const title = autoCreateWorkspaceMutation.isLoading - ? "Creating workspace..." - : "Create workspace"; - - const onCreateWorkspace = useCallback( - (workspace: Workspace) => { - navigate(`/@${workspace.owner_name}/${workspace.name}`); - }, - [navigate], - ); - - const automateWorkspaceCreation = useEffectEvent(async () => { - try { - const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({ - templateName, - organizationId, - defaultBuildParameters, - defaultName, - versionId: realizedVersionId, - }); - - onCreateWorkspace(newWorkspace); - } catch (err) { - searchParams.delete("mode"); - setSearchParams(searchParams); - } - }); - - useEffect(() => { - if (mode === "auto") { - void automateWorkspaceCreation(); - } - }, [automateWorkspaceCreation, mode]); - return ( <> - {pageTitle(title)} + + {pageTitle( + isAutoCreating ? "Creating workspace..." : "Create workspace", + )} + + {loadFormDataError && } - {isLoadingFormData || autoCreateWorkspaceMutation.isLoading ? ( + + {isLoadingFormData || isAutoCreating ? ( ) : ( { - navigate(-1); - }} + onCancel={() => navigate(-1)} + parameters={richParametersQuery.data!.filter( + (param) => !param.ephemeral, + )} onSubmit={async (request, owner) => { - if (realizedVersionId) { + if (versionId) { request = { ...request, template_id: undefined, - template_version_id: realizedVersionId, + template_version_id: versionId, }; } @@ -160,78 +146,184 @@ const CreateWorkspacePage: FC = () => { }; const useExternalAuth = (versionId: string | undefined) => { - const [externalAuthPollingState, setExternalAuthPollingState] = - useState("idle"); + const [pollingStatus, setPollingStatus] = + useState("idle"); - const startPollingExternalAuth = useCallback(() => { - setExternalAuthPollingState("polling"); + const startPolling = useCallback(() => { + setPollingStatus("polling"); }, []); - const { data: externalAuth } = useQuery( + const { data: authList } = useQuery( versionId ? { ...templateVersionExternalAuth(versionId), - refetchInterval: - externalAuthPollingState === "polling" ? 1000 : false, + refetchInterval: pollingStatus === "polling" ? 1000 : false, } : { enabled: false }, ); - const allSignedIn = externalAuth?.every((it) => it.authenticated); - useEffect(() => { - if (allSignedIn) { - setExternalAuthPollingState("idle"); - return; - } - - if (externalAuthPollingState !== "polling") { + if (pollingStatus !== "polling") { return; } - // Poll for a maximum of one minute - const quitPolling = setTimeout( - () => setExternalAuthPollingState("abandoned"), + const timeoutId = window.setTimeout( + () => setPollingStatus("abandoned"), 60_000, ); - return () => { - clearTimeout(quitPolling); - }; - }, [externalAuthPollingState, allSignedIn]); - return { - startPollingExternalAuth, - externalAuth, - externalAuthPollingState, - }; + return () => clearTimeout(timeoutId); + }, [pollingStatus]); + + const isAllSignedIn = authList?.every((it) => it.authenticated) ?? false; + + // Doing state sync inline, because doing it inside a useEffect call would add + // unnecessary renders and re-painting. + if (isAllSignedIn && pollingStatus === "polling") { + setPollingStatus("idle"); + } + + return { authList, isAllSignedIn, pollingStatus, startPolling } as const; +}; + +function getWorkspaceMode(params: URLSearchParams): CreateWorkspaceMode { + const paramMode = params.get("mode"); + if (createWorkspaceModes.includes(paramMode as CreateWorkspaceMode)) { + return paramMode as CreateWorkspaceMode; + } + + return "form"; +} + +function getDefaultName(mode: CreateWorkspaceMode, params: URLSearchParams) { + if (mode === "auto") { + return generateUniqueName(); + } + + const paramsName = params.get("name"); + if (mode === "duplicate" && paramsName) { + return `${paramsName}-copy`; + } + + return paramsName ?? ""; +} + +type AutomatedWorkspaceConfig = { + auto: boolean; + payload: AutoCreateWorkspaceOptions; + onSuccess: (newWorkspace: Workspace) => void; }; +function useAutomatedWorkspaceCreation(config: AutomatedWorkspaceConfig) { + // Duplicates some of the hook calls from the parent, but that was preferable + // to having the function arguments balloon in complexity + const [searchParams, setSearchParams] = useSearchParams(); + const queryClient = useQueryClient(); + const autoCreateWorkspaceMutation = useMutation( + autoCreateWorkspace(queryClient), + ); + + const automateWorkspaceCreation = useEffectEvent(async () => { + try { + const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync( + config.payload, + ); + + config.onSuccess(newWorkspace); + } catch (err) { + searchParams.delete("mode"); + setSearchParams(searchParams); + } + }); + + useEffect(() => { + if (config.auto) { + void automateWorkspaceCreation(); + } + }, [automateWorkspaceCreation, config.auto]); + + return autoCreateWorkspaceMutation.isLoading; +} + const getDefaultBuildParameters = ( urlSearchParams: URLSearchParams, ): WorkspaceBuildParameter[] => { - const buildValues: WorkspaceBuildParameter[] = []; - Array.from(urlSearchParams.keys()) + return [...urlSearchParams.keys()] .filter((key) => key.startsWith("param.")) - .forEach((key) => { - const name = key.replace("param.", ""); - const value = urlSearchParams.get(key) ?? ""; - buildValues.push({ name, value }); + .map((key) => { + return { + name: key.replace("param.", ""), + value: urlSearchParams.get(key) ?? "", + }; }); - return buildValues; }; -export const orderedTemplateParameters = ( - templateParameters?: TemplateVersionParameter[], -): TemplateVersionParameter[] => { - if (!templateParameters) { - return []; +function getDuplicationUrlParams( + workspaceParams: readonly WorkspaceBuildParameter[], + workspace: Workspace, +): URLSearchParams { + // Record type makes sure that every property key added starts with "param."; + // page is also set up to parse params with this prefix for auto mode + const consolidatedParams: Record<`param.${string}`, string> = {}; + + for (const p of workspaceParams) { + consolidatedParams[`param.${p.name}`] = p.value; } - const immutables = templateParameters.filter( - (parameter) => !parameter.mutable, + return new URLSearchParams({ + ...consolidatedParams, + mode: "duplicate" satisfies CreateWorkspaceMode, + name: workspace.name, + version: workspace.template_active_version_id, + }); +} + +/** + * Takes a workspace, and returns out a function that will navigate the user to + * the 'Create Workspace' page, pre-filling the form with as much information + * about the workspace as possible. + */ +// Meant to be consumed by components outside of this file +export function useWorkspaceDuplication(workspace: Workspace) { + const navigate = useNavigate(); + const buildParametersQuery = useQuery( + workspaceBuildParameters(workspace.latest_build.id), ); - const mutables = templateParameters.filter((parameter) => parameter.mutable); - return [...immutables, ...mutables]; + + // Not using useEffectEvent for this, even with the slightly more complicated + // dependency array, because useEffect isn't really an intended use case + const duplicateWorkspace = useCallback(() => { + const buildParams = buildParametersQuery.data; + if (buildParams === undefined) { + return; + } + + const newUrlParams = getDuplicationUrlParams(buildParams, workspace); + + // Necessary for giving modals/popups time to flush their state changes and + // close the popup before actually navigating. Otherwise, you risk the modal + // awkwardly hanging there during the page transition + void Promise.resolve().then(() => { + navigate({ + pathname: `/templates/${workspace.template_name}/workspace`, + search: newUrlParams.toString(), + }); + }); + }, [navigate, workspace, buildParametersQuery.data]); + + return { + duplicateWorkspace, + duplicationReady: !buildParametersQuery.isLoading, + } as const; +} + +export const orderTemplateParameters = ( + templateParameters?: readonly TemplateVersionParameter[], +) => { + return { + mutable: templateParameters?.filter((p) => p.mutable) ?? [], + immutable: templateParameters?.filter((p) => !p.mutable) ?? [], + } as const; }; const generateUniqueName = () => { diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index a4afc25bfc4c2..627f4a14d0b44 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -2,7 +2,7 @@ import TextField from "@mui/material/TextField"; import * as TypesGen from "api/typesGenerated"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { FormikContextType, useFormik } from "formik"; -import { FC, useEffect, useState } from "react"; +import { type FC, useEffect, useState, useReducer } from "react"; import { getFormHelpers, nameValidator, @@ -20,7 +20,7 @@ import { import { makeStyles } from "@mui/styles"; import { getInitialRichParameterValues, - useValidationSchemaForRichParameters, + validateRichParameters, } from "utils/richParameters"; import { ImmutableTemplateParametersSection, @@ -29,9 +29,15 @@ import { import { ExternalAuth } from "./ExternalAuth"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Stack } from "components/Stack/Stack"; -import { type ExternalAuthPollingState } from "./CreateWorkspacePage"; import { useSearchParams } from "react-router-dom"; import { CreateWSPermissions } from "./permissions"; +import { useTheme } from "@emotion/react"; +import { Alert } from "components/Alert/Alert"; +import { Margins } from "components/Margins/Margins"; +import { + type ExternalAuthPollingStatus, + type CreateWorkspaceMode, +} from "./CreateWorkspacePage"; export interface CreateWorkspacePageViewProps { error: unknown; @@ -40,13 +46,14 @@ export interface CreateWorkspacePageViewProps { template: TypesGen.Template; versionId?: string; externalAuth: TypesGen.TemplateVersionExternalAuth[]; - externalAuthPollingState: ExternalAuthPollingState; + externalAuthPollingStatus: ExternalAuthPollingStatus; startPollingExternalAuth: () => void; parameters: TypesGen.TemplateVersionParameter[]; defaultBuildParameters: TypesGen.WorkspaceBuildParameter[]; permissions: CreateWSPermissions; creatingWorkspace: boolean; onCancel: () => void; + mode: CreateWorkspaceMode; onSubmit: ( req: TypesGen.CreateWorkspaceRequest, owner: TypesGen.User, @@ -60,7 +67,7 @@ export const CreateWorkspacePageView: FC = ({ template, versionId, externalAuth, - externalAuthPollingState, + externalAuthPollingStatus, startPollingExternalAuth, parameters, defaultBuildParameters, @@ -68,14 +75,14 @@ export const CreateWorkspacePageView: FC = ({ creatingWorkspace, onSubmit, onCancel, + mode, }) => { const styles = useStyles(); const [owner, setOwner] = useState(defaultOwner); - const { verifyExternalAuth, externalAuthErrors } = - useExternalAuthVerification(externalAuth); const [searchParams] = useSearchParams(); const disabledParamsList = searchParams?.get("disable_params")?.split(","); + const authErrors = getAuthErrors(externalAuth); const form: FormikContextType = useFormik({ initialValues: { @@ -88,11 +95,13 @@ export const CreateWorkspacePageView: FC = ({ }, validationSchema: Yup.object({ name: nameValidator("Workspace Name"), - rich_parameter_values: useValidationSchemaForRichParameters(parameters), + rich_parameter_values: validateRichParameters(parameters), }), enableReinitialize: true, onSubmit: (request) => { - if (!verifyExternalAuth()) { + const errorCount = Object.keys(authErrors).length; + if (errorCount > 0) { + form.setSubmitting(false); return; } @@ -112,172 +121,186 @@ export const CreateWorkspacePageView: FC = ({ ); return ( - - - {Boolean(error) && } - {/* General info */} - - - - {versionId && versionId !== template.active_version_id && ( - - - - This parameter has been preset, and cannot be modified. - - - )} - - - + <> + {mode === "duplicate" && } + + + + {Boolean(error) && } - {permissions.createWorkspaceForUser && ( + {/* General info */} - { - setOwner(user ?? defaultOwner); - }} - label="Owner" - size="medium" + + {versionId && versionId !== template.active_version_id && ( + + + + This parameter has been preset, and cannot be modified. + + + )} + - )} - {externalAuth && externalAuth.length > 0 && ( - - - {externalAuth.map((auth) => ( - + + { + setOwner(user ?? defaultOwner); + }} + label="Owner" + size="medium" /> - ))} - - - )} + + + )} + + {externalAuth && externalAuth.length > 0 && ( + + + {externalAuth.map((auth) => ( + + ))} + + + )} + + {parameters && ( + <> + { + return { + ...getFieldHelpers( + "rich_parameter_values[" + index + "].value", + ), + onChange: async (value) => { + await form.setFieldValue( + "rich_parameter_values." + index, + { + name: parameter.name, + value: value, + }, + ); + }, + disabled: + disabledParamsList?.includes( + parameter.name.toLowerCase().replace(/ /g, "_"), + ) || form.isSubmitting, + }; + }} + /> - {parameters && ( - <> - { - return { - ...getFieldHelpers( - "rich_parameter_values[" + index + "].value", - ), - onChange: async (value) => { - await form.setFieldValue("rich_parameter_values." + index, { - name: parameter.name, - value: value, - }); - }, - disabled: - disabledParamsList?.includes( - parameter.name.toLowerCase().replace(/ /g, "_"), - ) || creatingWorkspace, - }; - }} - /> - { - return { - ...getFieldHelpers( - "rich_parameter_values[" + index + "].value", - ), - onChange: async (value) => { - await form.setFieldValue("rich_parameter_values." + index, { - name: parameter.name, - value: value, - }); - }, - disabled: - disabledParamsList?.includes( - parameter.name.toLowerCase().replace(/ /g, "_"), - ) || creatingWorkspace, - }; - }} - /> - - )} + { + return { + ...getFieldHelpers( + "rich_parameter_values[" + index + "].value", + ), + onChange: async (value) => { + await form.setFieldValue( + "rich_parameter_values." + index, + { + name: parameter.name, + value: value, + }, + ); + }, + disabled: + disabledParamsList?.includes( + parameter.name.toLowerCase().replace(/ /g, "_"), + ) || form.isSubmitting, + }; + }} + /> + + )} - - - + + + + ); }; -type ExternalAuthErrors = Record; +function getAuthErrors( + authList: readonly TypesGen.TemplateVersionExternalAuth[], +): Readonly> { + const authErrors: Record = {}; -const useExternalAuthVerification = ( - externalAuth: TypesGen.TemplateVersionExternalAuth[], -) => { - const [externalAuthErrors, setExternalAuthErrors] = - useState({}); - - // Clear errors when externalAuth is refreshed - useEffect(() => { - setExternalAuthErrors({}); - }, [externalAuth]); + for (const auth of authList) { + if (!auth.authenticated) { + authErrors[auth.id] = "You must authenticate to create a workspace!"; + } + } - const verifyExternalAuth = () => { - const errors: ExternalAuthErrors = {}; + return authErrors; +} - for (let i = 0; i < externalAuth.length; i++) { - const auth = externalAuth.at(i); - if (!auth) { - continue; - } - if (!auth.authenticated) { - errors[auth.id] = "You must authenticate to create a workspace!"; - } - } +function DuplicateWarningMessage() { + const [isDismissed, dismiss] = useReducer(() => true, false); + const theme = useTheme(); - setExternalAuthErrors(errors); - const isValid = Object.keys(errors).length === 0; - return isValid; - }; + if (isDismissed) { + return null; + } - return { - externalAuthErrors, - verifyExternalAuth, - }; -}; + // Setup looks a little hokey (having an Alert already fully configured to + // listen to dismissals, on top of more dismissal state), but relying solely + // on the Alert API wouldn't get rid of the div and horizontal margin helper + // after the dismiss happens. Not using CSS margins because those can be a + // style maintenance nightmare over time + return ( +
+ + + Duplicating a workspace only copies its parameters. No state from the + old workspace is copied over. + + +
+ ); +} const useStyles = makeStyles((theme) => ({ hasDescription: { diff --git a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx index 41dde1b8420ab..e9d85d17311cb 100644 --- a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx +++ b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx @@ -5,14 +5,14 @@ import Tooltip from "@mui/material/Tooltip"; import { type FC } from "react"; import { LoadingButton } from "components/LoadingButton/LoadingButton"; import { Stack } from "components/Stack/Stack"; -import { type ExternalAuthPollingState } from "./CreateWorkspacePage"; +import { type ExternalAuthPollingStatus } from "./CreateWorkspacePage"; export interface ExternalAuthProps { displayName: string; displayIcon: string; authenticated: boolean; authenticateURL: string; - externalAuthPollingState: ExternalAuthPollingState; + externalAuthPollingState: ExternalAuthPollingStatus; startPollingExternalAuth: () => void; error?: string; } diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 14834849a1bd3..9ebecbc27018b 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -120,6 +120,7 @@ const TemplateRow: FC<{ template: Template }> = ({ template }) => {