feat: setup connection to dynamic parameters websocket (#17393)

resolves coder/preview#57
This commit is contained in:
Jaayden Halko
2025-04-16 12:49:18 -04:00
committed by GitHub
parent f670bc31f5
commit 3d787da83b
4 changed files with 94 additions and 31 deletions
+26
View File
@@ -1009,6 +1009,32 @@ class ApiMethods {
return response.data;
};
templateVersionDynamicParameters = (
versionId: string,
{
onMessage,
onError,
}: {
onMessage: (response: TypesGen.DynamicParametersResponse) => void;
onError: (error: Error) => void;
},
): WebSocket => {
const socket = createWebSocket(
`/api/v2/templateversions/${versionId}/dynamic-parameters`,
);
socket.addEventListener("message", (event) =>
onMessage(JSON.parse(event.data) as TypesGen.DynamicParametersResponse),
);
socket.addEventListener("error", () => {
onError(new Error("Connection for dynamic parameters failed."));
socket.close();
});
return socket;
};
/**
* @param organization Can be the organization's ID or name
*/
@@ -1,4 +1,5 @@
import type {
NullHCLString,
PreviewParameter,
PreviewParameterOption,
WorkspaceBuildParameter,
@@ -156,10 +157,8 @@ const ParameterField: FC<ParameterFieldProps> = ({
disabled,
id,
}) => {
const value = parameter.value.valid ? parameter.value.value : "";
const defaultValue = parameter.default_value.valid
? parameter.default_value.value
: "";
const value = validValue(parameter.value);
const defaultValue = validValue(parameter.default_value);
switch (parameter.form_type) {
case "dropdown":
@@ -376,9 +375,7 @@ export const getInitialParameterValues = (
if (parameter.ephemeral) {
return {
name: parameter.name,
value: parameter.default_value.valid
? parameter.default_value.value
: "",
value: validValue(parameter.default_value),
};
}
@@ -390,15 +387,19 @@ export const getInitialParameterValues = (
name: parameter.name,
value:
autofillParam &&
isValidValue(parameter, autofillParam) &&
isValidParameterOption(parameter, autofillParam) &&
autofillParam.value
? autofillParam.value
: "",
: validValue(parameter.default_value),
};
});
};
const isValidValue = (
const validValue = (value: NullHCLString) => {
return value.valid ? value.value : "";
};
const isValidParameterOption = (
previewParam: PreviewParameter,
buildParam: WorkspaceBuildParameter,
) => {
@@ -409,7 +410,7 @@ const isValidValue = (
return validValues.includes(buildParam.value);
}
return true;
return false;
};
export const useValidationSchemaForDynamicParameters = (
@@ -9,7 +9,6 @@ import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
import type {
DynamicParametersRequest,
DynamicParametersResponse,
Template,
Workspace,
} from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
@@ -32,6 +31,7 @@ import type { AutofillBuildParameter } from "utils/richParameters";
import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental";
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
import { API } from "api/api";
import {
type CreateWorkspacePermissions,
createWorkspaceChecks,
@@ -47,8 +47,9 @@ const CreateWorkspacePageExperimental: FC = () => {
const [currentResponse, setCurrentResponse] =
useState<DynamicParametersResponse | null>(null);
const [wsResponseId, setWSResponseId] = useState<number>(0);
const sendMessage = (message: DynamicParametersRequest) => {};
const [wsResponseId, setWSResponseId] = useState<number>(-1);
const ws = useRef<WebSocket | null>(null);
const [wsError, setWsError] = useState<Error | null>(null);
const customVersionId = searchParams.get("version") ?? undefined;
const defaultName = searchParams.get("name");
@@ -80,6 +81,49 @@ const CreateWorkspacePageExperimental: FC = () => {
const realizedVersionId =
customVersionId ?? templateQuery.data?.active_version_id;
const onMessage = useCallback((response: DynamicParametersResponse) => {
setCurrentResponse((prev) => {
if (prev?.id === response.id) {
return prev;
}
return response;
});
}, []);
// Initialize the WebSocket connection when there is a valid template version ID
useEffect(() => {
if (!realizedVersionId) {
return;
}
const socket = API.templateVersionDynamicParameters(realizedVersionId, {
onMessage,
onError: (error) => {
setWsError(error);
},
});
ws.current = socket;
return () => {
socket.close();
};
}, [realizedVersionId, onMessage]);
const sendMessage = useCallback((formValues: Record<string, string>) => {
setWSResponseId((prevId) => {
const request: DynamicParametersRequest = {
id: prevId + 1,
inputs: formValues,
};
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(request));
return prevId + 1;
}
return prevId;
});
}, []);
const organizationId = templateQuery.data?.organization_id;
const {
@@ -90,7 +134,9 @@ const CreateWorkspacePageExperimental: FC = () => {
} = useExternalAuth(realizedVersionId);
const isLoadingFormData =
templateQuery.isLoading || permissionsQuery.isLoading;
ws.current?.readyState !== WebSocket.OPEN ||
templateQuery.isLoading ||
permissionsQuery.isLoading;
const loadFormDataError = templateQuery.error ?? permissionsQuery.error;
const title = autoCreateWorkspaceMutation.isLoading
@@ -189,11 +235,12 @@ const CreateWorkspacePageExperimental: FC = () => {
<CreateWorkspacePageViewExperimental
mode={mode}
defaultName={defaultName}
diagnostics={currentResponse.diagnostics}
diagnostics={currentResponse?.diagnostics ?? []}
disabledParams={disabledParams}
defaultOwner={me}
autofillParameters={autofillParameters}
error={
wsError ||
createWorkspaceMutation.error ||
autoCreateError ||
loadFormDataError ||
@@ -210,7 +257,6 @@ const CreateWorkspacePageExperimental: FC = () => {
parameters={sortedParams}
presets={templateVersionPresetsQuery.data ?? []}
creatingWorkspace={createWorkspaceMutation.isLoading}
setWSResponseId={setWSResponseId}
sendMessage={sendMessage}
onCancel={() => {
navigate(-1);
@@ -67,8 +67,7 @@ export interface CreateWorkspacePageViewExperimentalProps {
owner: TypesGen.User,
) => void;
resetMutation: () => void;
sendMessage: (message: DynamicParametersRequest) => void;
setWSResponseId: (value: React.SetStateAction<number>) => void;
sendMessage: (message: Record<string, string>) => void;
startPollingExternalAuth: () => void;
}
@@ -95,7 +94,6 @@ export const CreateWorkspacePageViewExperimental: FC<
onCancel,
resetMutation,
sendMessage,
setWSResponseId,
startPollingExternalAuth,
}) => {
const [owner, setOwner] = useState(defaultOwner);
@@ -222,15 +220,7 @@ export const CreateWorkspacePageViewExperimental: FC<
// Update the input for the changed parameter
formInputs[parameter.name] = value;
setWSResponseId((prevId) => {
const newId = prevId + 1;
const request: DynamicParametersRequest = {
id: newId,
inputs: formInputs,
};
sendMessage(request);
return newId;
});
sendMessage(formInputs);
};
const { debounced: handleChangeDebounced } = useDebouncedFunction(
@@ -240,7 +230,7 @@ export const CreateWorkspacePageViewExperimental: FC<
value: string,
) => {
await form.setFieldValue(parameterField, {
name: parameter.form_type,
name: parameter.name,
value,
});
sendDynamicParamsRequest(parameter, value);
@@ -257,7 +247,7 @@ export const CreateWorkspacePageViewExperimental: FC<
handleChangeDebounced(parameter, parameterField, value);
} else {
await form.setFieldValue(parameterField, {
name: parameter.form_type,
name: parameter.name,
value,
});
sendDynamicParamsRequest(parameter, value);