feat: handle update build for dynamic params (#18226)

resolves coder/preview#110

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
This commit is contained in:
Jaayden Halko
2025-06-05 16:35:23 -04:00
committed by GitHub
co-authored by blink-so[bot]
parent c339066be3
commit 508fba83dd
15 changed files with 431 additions and 132 deletions
@@ -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,
};
},
});
};
@@ -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 (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Update workspace parameters</DialogTitle>
<DialogDescription>
This template has{" "}
<strong className="text-content-primary">
{missedParameters.length} new parameter
{missedParameters.length === 1 ? "" : "s"}
</strong>{" "}
that must be configured to complete the update.
</DialogDescription>
<DialogDescription>
Would you like to go to the workspace parameters page to review and
update these parameters before continuing?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={onClose} variant="outline">
Cancel
</Button>
<Button onClick={handleGoToParameters}>
Go to workspace parameters
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
@@ -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<WorkspaceMoreActionsProps> = ({
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<WorkspaceMoreActionsProps> = ({
// 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<WorkspaceMoreActionsProps> = ({
onClose={() => setIsDownloadDialogOpen(false)}
/>
<UpdateBuildParametersDialog
missedParameters={
changeVersionMutation.error instanceof MissingBuildParameters
? changeVersionMutation.error.parameters
: []
}
open={changeVersionMutation.error instanceof MissingBuildParameters}
onClose={() => {
changeVersionMutation.reset();
}}
onUpdate={(buildParameters) => {
if (changeVersionMutation.error instanceof MissingBuildParameters) {
changeVersionMutation.mutate({
versionId: changeVersionMutation.error.versionId,
buildParameters,
});
{!isDynamicParametersEnabled || optOutQuery.data?.optedOut ? (
<UpdateBuildParametersDialog
missedParameters={
changeVersionMutation.error instanceof MissingBuildParameters
? changeVersionMutation.error.parameters
: []
}
}}
/>
open={changeVersionMutation.error instanceof MissingBuildParameters}
onClose={() => {
changeVersionMutation.reset();
}}
onUpdate={(buildParameters) => {
if (changeVersionMutation.error instanceof MissingBuildParameters) {
changeVersionMutation.mutate({
versionId: changeVersionMutation.error.versionId,
buildParameters,
});
}
}}
/>
) : (
<UpdateBuildParametersDialogExperimental
missedParameters={
changeVersionMutation.error instanceof MissingBuildParameters
? changeVersionMutation.error.parameters
: []
}
open={changeVersionMutation.error instanceof MissingBuildParameters}
onClose={() => {
changeVersionMutation.reset();
}}
workspaceOwnerName={workspace.owner_name}
workspaceName={workspace.name}
templateVersionId={
changeVersionMutation.error instanceof MissingBuildParameters
? changeVersionMutation.error?.versionId
: undefined
}
/>
)}
<ChangeWorkspaceVersionDialog
workspace={workspace}
@@ -6,9 +6,14 @@ import type {
WorkspaceBuild,
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Loader } from "components/Loader/Loader";
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
import { useDashboard } from "modules/dashboard/useDashboard";
import { useDynamicParametersOptOut } from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut";
import { UpdateBuildParametersDialog } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog";
import { UpdateBuildParametersDialogExperimental } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental";
import { type FC, useState } from "react";
import { useMutation, useQueryClient } from "react-query";
@@ -51,8 +56,21 @@ export const useWorkspaceUpdate = ({
setIsConfirmingUpdate(true);
};
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 confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => {
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<UpdateConfirmationDialogProps> = ({
};
type MissingBuildParametersDialogProps = {
workspace: Workspace;
error: unknown;
onClose: () => void;
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void;
};
const MissingBuildParametersDialog: FC<MissingBuildParametersDialogProps> = ({
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 <ErrorAlert error={optOutQuery.error} />;
}
if (isDynamicParametersEnabled && !optOutQuery.data) {
return <Loader />;
}
// If dynamic parameters experiment is not enabled, or if opted out, use classic dialog
const shouldUseClassicDialog =
!isDynamicParametersEnabled || optOutQuery.data?.optedOut;
return shouldUseClassicDialog ? (
<UpdateBuildParametersDialog
missedParameters={
error instanceof MissingBuildParameters ? error.parameters : []
}
open={error instanceof MissingBuildParameters}
missedParameters={missedParameters}
open={isOpen}
{...dialogProps}
/>
) : (
<UpdateBuildParametersDialogExperimental
missedParameters={missedParameters}
open={isOpen}
onClose={dialogProps.onClose}
workspaceOwnerName={workspace.owner_name}
workspaceName={workspace.name}
templateVersionId={versionId}
/>
);
};