diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5463ad7a44dd6..28807bd547c2a 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -24,7 +24,10 @@ import type dayjs from "dayjs"; import userAgentParser from "ua-parser-js"; import { OneWayWebSocket } from "../utils/OneWayWebSocket"; import { delay } from "../utils/delay"; -import type { PostWorkspaceUsageRequest } from "./typesGenerated"; +import type { + DynamicParametersRequest, + PostWorkspaceUsageRequest, +} from "./typesGenerated"; import * as TypesGen from "./typesGenerated"; const getMissingParameters = ( @@ -73,8 +76,10 @@ const getMissingParameters = ( if (templateParameter.options.length === 0) { continue; } - - // Check if there is a new value + // For multi-select, extra steps are necessary to JSON parse the value. + if (templateParameter.form_type === "multi-select") { + continue; + } let buildParameter = newBuildParameters.find( (p) => p.name === templateParameter.name, ); @@ -231,7 +236,7 @@ export const watchWorkspaceAgentLogs = ( /** * WebSocket compression in Safari (confirmed in 16.5) is broken when * the server sends large messages. The following error is seen: - * WebSocket connection to 'wss://...' failed: The operation couldn’t be completed. + * WebSocket connection to 'wss://...' failed: The operation couldn't be completed. */ if (userAgentParser(navigator.userAgent).browser.name === "Safari") { searchParams.set("no_compression", ""); @@ -990,6 +995,17 @@ class ApiMethods { return response.data; }; + getTemplateVersionDynamicParameters = async ( + versionId: string, + data: TypesGen.DynamicParametersRequest, + ): Promise => { + const response = await this.axios.post( + `/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`, + data, + ); + return response.data; + }; + getTemplateVersionRichParameters = async ( versionId: string, ): Promise => { @@ -2132,6 +2148,38 @@ class ApiMethods { await this.axios.delete(`/api/v2/licenses/${licenseId}`); }; + getDynamicParameters = async ( + templateVersionId: string, + ownerId: string, + oldBuildParameters: TypesGen.WorkspaceBuildParameter[], + ) => { + const request: DynamicParametersRequest = { + id: 1, + owner_id: ownerId, + inputs: Object.fromEntries( + new Map(oldBuildParameters.map((param) => [param.name, param.value])), + ), + }; + + const dynamicParametersResponse = + await this.getTemplateVersionDynamicParameters( + templateVersionId, + request, + ); + + return dynamicParametersResponse.parameters.map((p) => ({ + ...p, + description_plaintext: p.description || "", + default_value: p.default_value?.valid ? p.default_value.value : "", + options: p.options + ? p.options.map((opt) => ({ + ...opt, + value: opt.value?.valid ? opt.value.value : "", + })) + : [], + })); + }; + /** Steps to change the workspace version * - Get the latest template to access the latest active version * - Get the current build parameters @@ -2145,11 +2193,23 @@ class ApiMethods { workspace: TypesGen.Workspace, templateVersionId: string, newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + isDynamicParametersEnabled = false, ): Promise => { - const [currentBuildParameters, templateParameters] = await Promise.all([ - this.getWorkspaceBuildParameters(workspace.latest_build.id), - this.getTemplateVersionRichParameters(templateVersionId), - ]); + const currentBuildParameters = await this.getWorkspaceBuildParameters( + workspace.latest_build.id, + ); + + let templateParameters: TypesGen.TemplateVersionParameter[] = []; + if (isDynamicParametersEnabled) { + templateParameters = await this.getDynamicParameters( + templateVersionId, + workspace.owner_id, + currentBuildParameters, + ); + } else { + templateParameters = + await this.getTemplateVersionRichParameters(templateVersionId); + } const missingParameters = getMissingParameters( currentBuildParameters, @@ -2180,6 +2240,7 @@ class ApiMethods { updateWorkspace = async ( workspace: TypesGen.Workspace, newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], + isDynamicParametersEnabled = false, ): Promise => { const [template, oldBuildParameters] = await Promise.all([ this.getTemplate(workspace.template_id), @@ -2187,8 +2248,19 @@ class ApiMethods { ]); const activeVersionId = template.active_version_id; - const templateParameters = - await this.getTemplateVersionRichParameters(activeVersionId); + + let templateParameters: TypesGen.TemplateVersionParameter[] = []; + + if (isDynamicParametersEnabled) { + templateParameters = await this.getDynamicParameters( + activeVersionId, + workspace.owner_id, + oldBuildParameters, + ); + } else { + templateParameters = + await this.getTemplateVersionRichParameters(activeVersionId); + } const missingParameters = getMissingParameters( oldBuildParameters, diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 6c6a1aa19825c..5a4cdb46dd4e9 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -163,6 +163,7 @@ export const updateDeadline = ( export const changeVersion = ( workspace: Workspace, queryClient: QueryClient, + isDynamicParametersEnabled: boolean, ) => { return { mutationFn: ({ @@ -172,7 +173,12 @@ export const changeVersion = ( versionId: string; buildParameters?: WorkspaceBuildParameter[]; }) => { - return API.changeWorkspaceVersion(workspace, versionId, buildParameters); + return API.changeWorkspaceVersion( + workspace, + versionId, + buildParameters, + isDynamicParametersEnabled, + ); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); @@ -185,8 +191,18 @@ export const updateWorkspace = ( queryClient: QueryClient, ) => { return { - mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => { - return API.updateWorkspace(workspace, buildParameters); + mutationFn: ({ + buildParameters, + isDynamicParametersEnabled, + }: { + buildParameters?: WorkspaceBuildParameter[]; + isDynamicParametersEnabled: boolean; + }) => { + return API.updateWorkspace( + workspace, + buildParameters, + isDynamicParametersEnabled, + ); }, onSuccess: async (build: WorkspaceBuild) => { await updateWorkspaceBuild(build, queryClient); diff --git a/site/src/components/Dialog/Dialog.tsx b/site/src/components/Dialog/Dialog.tsx index 7dbd536204254..2ec8ab40781c7 100644 --- a/site/src/components/Dialog/Dialog.tsx +++ b/site/src/components/Dialog/Dialog.tsx @@ -45,7 +45,7 @@ export const DialogContent = forwardRef< > = ({ }) => (
(({ className, ...props }, ref) => ( )); diff --git a/site/src/modules/workspaces/DynamicParameter/useDynamicParametersOptOut.ts b/site/src/modules/workspaces/DynamicParameter/useDynamicParametersOptOut.ts new file mode 100644 index 0000000000000..6401f5f7f3564 --- /dev/null +++ b/site/src/modules/workspaces/DynamicParameter/useDynamicParametersOptOut.ts @@ -0,0 +1,42 @@ +import { useQuery } from "react-query"; + +export const optOutKey = (id: string): string => `parameters.${id}.optOut`; + +interface UseDynamicParametersOptOutOptions { + templateId: string | undefined; + templateUsesClassicParameters: boolean | undefined; + enabled: boolean; +} + +export const useDynamicParametersOptOut = ({ + templateId, + templateUsesClassicParameters, + enabled, +}: UseDynamicParametersOptOutOptions) => { + return useQuery({ + enabled: !!templateId && enabled, + queryKey: ["dynamicParametersOptOut", templateId], + queryFn: () => { + if (!templateId) { + // This should not happen if enabled is working correctly, + // but as a type guard and sanity check. + throw new Error("templateId is required"); + } + const localStorageKey = optOutKey(templateId); + const storedOptOutString = localStorage.getItem(localStorageKey); + + let optedOut: boolean; + + if (storedOptOutString !== null) { + optedOut = storedOptOutString === "true"; + } else { + optedOut = Boolean(templateUsesClassicParameters); + } + + return { + templateId, + optedOut, + }; + }, + }); +}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx new file mode 100644 index 0000000000000..04bb92a5e79b2 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental.tsx @@ -0,0 +1,71 @@ +import type { TemplateVersionParameter } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "components/Dialog/Dialog"; +import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; + +type UpdateBuildParametersDialogExperimentalProps = { + open: boolean; + onClose: () => void; + missedParameters: TemplateVersionParameter[]; + workspaceOwnerName: string; + workspaceName: string; + templateVersionId: string | undefined; +}; + +export const UpdateBuildParametersDialogExperimental: FC< + UpdateBuildParametersDialogExperimentalProps +> = ({ + missedParameters, + open, + onClose, + workspaceOwnerName, + workspaceName, + templateVersionId, +}) => { + const navigate = useNavigate(); + + const handleGoToParameters = () => { + onClose(); + navigate( + `/@${workspaceOwnerName}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`, + ); + }; + + return ( + !isOpen && onClose()}> + + + Update workspace parameters + + This template has{" "} + + {missedParameters.length} new parameter + {missedParameters.length === 1 ? "" : "s"} + {" "} + that must be configured to complete the update. + + + Would you like to go to the workspace parameters page to review and + update these parameters before continuing? + + + + + + + + + ); +}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 22e9638ee7caa..8cdbafad435a3 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -21,12 +21,15 @@ import { SettingsIcon, TrashIcon, } from "lucide-react"; +import { useDashboard } from "modules/dashboard/useDashboard"; +import { useDynamicParametersOptOut } from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut"; import { type FC, useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; +import { UpdateBuildParametersDialogExperimental } from "./UpdateBuildParametersDialogExperimental"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; @@ -40,6 +43,15 @@ export const WorkspaceMoreActions: FC = ({ disabled, }) => { const queryClient = useQueryClient(); + const { experiments } = useDashboard(); + const isDynamicParametersEnabled = experiments.includes("dynamic-parameters"); + + const optOutQuery = useDynamicParametersOptOut({ + templateId: workspace.template_id, + templateUsesClassicParameters: + workspace.template_use_classic_parameter_flow, + enabled: isDynamicParametersEnabled, + }); // Permissions const { data: permissions } = useQuery(workspacePermissions(workspace)); @@ -50,7 +62,7 @@ export const WorkspaceMoreActions: FC = ({ // Change version const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const changeVersionMutation = useMutation( - changeVersion(workspace, queryClient), + changeVersion(workspace, queryClient, optOutQuery.data?.optedOut === false), ); // Delete @@ -142,25 +154,46 @@ export const WorkspaceMoreActions: FC = ({ onClose={() => setIsDownloadDialogOpen(false)} /> - { - changeVersionMutation.reset(); - }} - onUpdate={(buildParameters) => { - if (changeVersionMutation.error instanceof MissingBuildParameters) { - changeVersionMutation.mutate({ - versionId: changeVersionMutation.error.versionId, - buildParameters, - }); + {!isDynamicParametersEnabled || optOutQuery.data?.optedOut ? ( + + open={changeVersionMutation.error instanceof MissingBuildParameters} + onClose={() => { + changeVersionMutation.reset(); + }} + onUpdate={(buildParameters) => { + if (changeVersionMutation.error instanceof MissingBuildParameters) { + changeVersionMutation.mutate({ + versionId: changeVersionMutation.error.versionId, + buildParameters, + }); + } + }} + /> + ) : ( + { + changeVersionMutation.reset(); + }} + workspaceOwnerName={workspace.owner_name} + workspaceName={workspace.name} + templateVersionId={ + changeVersionMutation.error instanceof MissingBuildParameters + ? changeVersionMutation.error?.versionId + : undefined + } + /> + )} { - updateWorkspaceMutation.mutate(buildParameters); + updateWorkspaceMutation.mutate({ + buildParameters, + isDynamicParametersEnabled: optOutQuery.data?.optedOut === false, + }); setIsConfirmingUpdate(false); }; @@ -67,6 +85,7 @@ export const useWorkspaceUpdate = ({ latestVersion, }, missingBuildParameters: { + workspace, error: updateWorkspaceMutation.error, onClose: () => { updateWorkspaceMutation.reset(); @@ -134,22 +153,57 @@ const UpdateConfirmationDialog: FC = ({ }; type MissingBuildParametersDialogProps = { + workspace: Workspace; error: unknown; onClose: () => void; onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void; }; const MissingBuildParametersDialog: FC = ({ + workspace, error, ...dialogProps }) => { - return ( + const { experiments } = useDashboard(); + const isDynamicParametersEnabled = experiments.includes("dynamic-parameters"); + const optOutQuery = useDynamicParametersOptOut({ + templateId: workspace.template_id, + templateUsesClassicParameters: + workspace.template_use_classic_parameter_flow, + enabled: isDynamicParametersEnabled, + }); + + const missedParameters = + error instanceof MissingBuildParameters ? error.parameters : []; + const versionId = + error instanceof MissingBuildParameters ? error.versionId : undefined; + const isOpen = error instanceof MissingBuildParameters; + + if (optOutQuery.isError) { + return ; + } + if (isDynamicParametersEnabled && !optOutQuery.data) { + return ; + } + + // If dynamic parameters experiment is not enabled, or if opted out, use classic dialog + const shouldUseClassicDialog = + !isDynamicParametersEnabled || optOutQuery.data?.optedOut; + + return shouldUseClassicDialog ? ( + ) : ( + ); }; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx index 4f2d0a4e4f8f7..fe63391916128 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspaceExperimentRouter.tsx @@ -2,6 +2,10 @@ import { templateByName } from "api/queries/templates"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; +import { + optOutKey, + useDynamicParametersOptOut, +} from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut"; import type { FC } from "react"; import { useQuery } from "react-query"; import { useParams } from "react-router-dom"; @@ -11,39 +15,26 @@ import { ExperimentalFormContext } from "./ExperimentalFormContext"; const CreateWorkspaceExperimentRouter: FC = () => { const { experiments } = useDashboard(); - const dynamicParametersEnabled = experiments.includes("dynamic-parameters"); + const isDynamicParametersEnabled = experiments.includes("dynamic-parameters"); const { organization: organizationName = "default", template: templateName } = useParams() as { organization?: string; template: string }; const templateQuery = useQuery({ ...templateByName(organizationName, templateName), - enabled: dynamicParametersEnabled, + enabled: isDynamicParametersEnabled, }); - const optOutQuery = useQuery({ + const optOutQuery = useDynamicParametersOptOut({ + templateId: templateQuery.data?.id, + templateUsesClassicParameters: + templateQuery.data?.use_classic_parameter_flow, enabled: !!templateQuery.data, - queryKey: [organizationName, "template", templateQuery.data?.id, "optOut"], - queryFn: () => { - const templateId = templateQuery.data?.id; - const localStorageKey = optOutKey(templateId ?? ""); - const storedOptOutString = localStorage.getItem(localStorageKey); - - let optOutResult: boolean; - - if (storedOptOutString !== null) { - optOutResult = storedOptOutString === "true"; - } else { - optOutResult = !!templateQuery.data?.use_classic_parameter_flow; - } - - return { - templateId: templateId, - optedOut: optOutResult, - }; - }, }); - if (dynamicParametersEnabled) { + if (isDynamicParametersEnabled) { + if (templateQuery.isError) { + return ; + } if (optOutQuery.isError) { return ; } @@ -77,5 +68,3 @@ const CreateWorkspaceExperimentRouter: FC = () => { }; export default CreateWorkspaceExperimentRouter; - -const optOutKey = (id: string) => `parameters.${id}.optOut`; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 3f217a86a3aad..fb95d0c883627 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -305,16 +305,20 @@ describe("WorkspacePage", () => { // Check if the update was called using the values from the form await waitFor(() => { - expect(API.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [ - { - name: MockTemplateVersionParameter1.name, - value: "some-value", - }, - { - name: MockTemplateVersionParameter2.name, - value: "2", - }, - ]); + expect(API.updateWorkspace).toBeCalledWith( + MockOutdatedWorkspace, + [ + { + name: MockTemplateVersionParameter1.name, + value: "some-value", + }, + { + name: MockTemplateVersionParameter2.name, + value: "2", + }, + ], + false, + ); }); }); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx index e7f57108f8e54..476a764ac9204 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersExperimentRouter.tsx @@ -1,8 +1,11 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; +import { + optOutKey, + useDynamicParametersOptOut, +} from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut"; import type { FC } from "react"; -import { useQuery } from "react-query"; import { ExperimentalFormContext } from "../../CreateWorkspacePage/ExperimentalFormContext"; import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import WorkspaceParametersPage from "./WorkspaceParametersPage"; @@ -11,45 +14,21 @@ import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperi const WorkspaceParametersExperimentRouter: FC = () => { const { experiments } = useDashboard(); const workspace = useWorkspaceSettings(); - const dynamicParametersEnabled = experiments.includes("dynamic-parameters"); + const isDynamicParametersEnabled = experiments.includes("dynamic-parameters"); - const optOutQuery = useQuery({ - enabled: dynamicParametersEnabled, - queryKey: [ - "workspace", - workspace.id, - "template_id", - workspace.template_id, - "optOut", - ], - queryFn: () => { - const templateId = workspace.template_id; - const workspaceId = workspace.id; - const localStorageKey = optOutKey(templateId); - const storedOptOutString = localStorage.getItem(localStorageKey); - - let optOutResult: boolean; - - if (storedOptOutString !== null) { - optOutResult = storedOptOutString === "true"; - } else { - optOutResult = Boolean(workspace.template_use_classic_parameter_flow); - } - - return { - templateId, - workspaceId, - optedOut: optOutResult, - }; - }, + const optOutQuery = useDynamicParametersOptOut({ + templateId: workspace.template_id, + templateUsesClassicParameters: + workspace.template_use_classic_parameter_flow, + enabled: isDynamicParametersEnabled, }); - if (dynamicParametersEnabled) { - if (optOutQuery.isLoading) { - return ; + if (isDynamicParametersEnabled) { + if (optOutQuery.isError) { + return ; } if (!optOutQuery.data) { - return ; + return ; } const toggleOptedOut = () => { @@ -79,5 +58,3 @@ const WorkspaceParametersExperimentRouter: FC = () => { }; export default WorkspaceParametersExperimentRouter; - -const optOutKey = (id: string) => `parameters.${id}.optOut`; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 37baf9c3a1240..5fa3033542782 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -24,7 +24,7 @@ import type { FC } from "react"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery } from "react-query"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import type { AutofillBuildParameter } from "utils/richParameters"; @@ -40,6 +40,8 @@ const WorkspaceParametersPageExperimental: FC = () => { const workspace = useWorkspaceSettings(); const navigate = useNavigate(); const experimentalFormContext = useContext(ExperimentalFormContext); + const [searchParams] = useSearchParams(); + const templateVersionId = searchParams.get("templateVersionId") ?? undefined; // autofill the form with the workspace build parameters from the latest build const { @@ -107,10 +109,11 @@ const WorkspaceParametersPageExperimental: FC = () => { }); useEffect(() => { - if (!workspace.latest_build.template_version_id) return; + if (!templateVersionId && !workspace.latest_build.template_version_id) + return; const socket = API.templateVersionDynamicParameters( - workspace.latest_build.template_version_id, + templateVersionId ?? workspace.latest_build.template_version_id, { onMessage, onError: (error) => { @@ -136,12 +139,17 @@ const WorkspaceParametersPageExperimental: FC = () => { return () => { socket.close(); }; - }, [workspace.latest_build.template_version_id, onMessage]); + }, [ + templateVersionId, + workspace.latest_build.template_version_id, + onMessage, + ]); const updateParameters = useMutation({ mutationFn: (buildParameters: WorkspaceBuildParameter[]) => API.postWorkspaceBuild(workspace.id, { transition: "start", + template_version_id: templateVersionId, rich_parameter_values: buildParameters, }), onSuccess: () => { @@ -250,6 +258,7 @@ const WorkspaceParametersPageExperimental: FC = () => { {sortedParams.length > 0 ? ( void; sendMessage: (formValues: Record) => void; + templateVersionId: string | undefined; }; export const WorkspaceParametersPageViewExperimental: FC< @@ -44,6 +46,7 @@ export const WorkspaceParametersPageViewExperimental: FC< onSubmit, sendMessage, onCancel, + templateVersionId, }) => { const autofillByName = Object.fromEntries( autofillParameters.map((param) => [param.name, param]), @@ -152,6 +155,15 @@ export const WorkspaceParametersPageViewExperimental: FC<
)} + {(templateVersionId || workspace.latest_build.template_version_id) && ( +
+ +

+ {templateVersionId ?? workspace.latest_build.template_version_id} +

+
+ )} +
{standardParameters.length > 0 && (
@@ -236,10 +248,21 @@ export const WorkspaceParametersPageViewExperimental: FC< diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index c8577f191d47e..94c12f0372b59 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -123,8 +123,8 @@ describe("WorkspacesPage", () => { await waitFor(() => { expect(updateWorkspace).toHaveBeenCalledTimes(2); }); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3]); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3], [], false); }); it("warns about and updates running workspaces", async () => { @@ -160,9 +160,9 @@ describe("WorkspacesPage", () => { await waitFor(() => { expect(updateWorkspace).toHaveBeenCalledTimes(3); }); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false); }); it("warns about and ignores dormant workspaces", async () => { @@ -199,8 +199,8 @@ describe("WorkspacesPage", () => { await waitFor(() => { expect(updateWorkspace).toHaveBeenCalledTimes(2); }); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false); }); it("warns about running workspaces and then dormant workspaces", async () => { @@ -241,9 +241,9 @@ describe("WorkspacesPage", () => { await waitFor(() => { expect(updateWorkspace).toHaveBeenCalledTimes(3); }); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]); - expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3]); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false); + expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3], [], false); }); }); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index b6309638e209d..acdded15d4bc9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -37,6 +37,8 @@ function useSafeSearchParams() { } const WorkspacesPage: FC = () => { + const { experiments } = useDashboard(); + const isDynamicParametersEnabled = experiments.includes("dynamic-parameters"); const queryClient = useQueryClient(); // If we use a useSearchParams for each hook, the values will not be in sync. // So we have to use a single one, centralizing the values, and pass it to @@ -162,7 +164,10 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} open={confirmingBatchAction === "update"} onConfirm={async () => { - await batchActions.updateAll(checkedWorkspaces); + await batchActions.updateAll({ + workspaces: checkedWorkspaces, + isDynamicParametersEnabled, + }); setConfirmingBatchAction(null); }} onClose={() => { diff --git a/site/src/pages/WorkspacesPage/batchActions.tsx b/site/src/pages/WorkspacesPage/batchActions.tsx index cbec017eb8583..806c7a03afddb 100644 --- a/site/src/pages/WorkspacesPage/batchActions.tsx +++ b/site/src/pages/WorkspacesPage/batchActions.tsx @@ -45,11 +45,15 @@ export function useBatchActions(options: UseBatchActionsProps) { }); const updateAllMutation = useMutation({ - mutationFn: (workspaces: readonly Workspace[]) => { + mutationFn: (payload: { + workspaces: readonly Workspace[]; + isDynamicParametersEnabled: boolean; + }) => { + const { workspaces, isDynamicParametersEnabled } = payload; return Promise.all( workspaces .filter((w) => w.outdated && !w.dormant_at) - .map((w) => API.updateWorkspace(w)), + .map((w) => API.updateWorkspace(w, [], isDynamicParametersEnabled)), ); }, onSuccess,