mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
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:
+82
-10
@@ -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<TypesGen.DynamicParametersResponse> => {
|
||||
const response = await this.axios.post(
|
||||
`/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getTemplateVersionRichParameters = async (
|
||||
versionId: string,
|
||||
): Promise<TypesGen.TemplateVersionParameter[]> => {
|
||||
@@ -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<TypesGen.WorkspaceBuild> => {
|
||||
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<TypesGen.WorkspaceBuild> => {
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const DialogContent = forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-4
|
||||
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
|
||||
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
|
||||
translate-x-[-50%] translate-y-[-50%]
|
||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||
@@ -68,7 +68,7 @@ export const DialogHeader: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
"flex flex-col space-y-5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -108,7 +108,7 @@ export const DialogDescription = forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-content-secondary", className)}
|
||||
className={cn("text-sm text-content-secondary font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
+71
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 <ErrorAlert error={templateQuery.error} />;
|
||||
}
|
||||
if (optOutQuery.isError) {
|
||||
return <ErrorAlert error={optOutQuery.error} />;
|
||||
}
|
||||
@@ -77,5 +68,3 @@ const CreateWorkspaceExperimentRouter: FC = () => {
|
||||
};
|
||||
|
||||
export default CreateWorkspaceExperimentRouter;
|
||||
|
||||
const optOutKey = (id: string) => `parameters.${id}.optOut`;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+14
-37
@@ -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 <Loader />;
|
||||
if (isDynamicParametersEnabled) {
|
||||
if (optOutQuery.isError) {
|
||||
return <ErrorAlert error={optOutQuery.error} />;
|
||||
}
|
||||
if (!optOutQuery.data) {
|
||||
return <ErrorAlert error={optOutQuery.error} />;
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const toggleOptedOut = () => {
|
||||
@@ -79,5 +58,3 @@ const WorkspaceParametersExperimentRouter: FC = () => {
|
||||
};
|
||||
|
||||
export default WorkspaceParametersExperimentRouter;
|
||||
|
||||
const optOutKey = (id: string) => `parameters.${id}.optOut`;
|
||||
|
||||
+13
-4
@@ -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 ? (
|
||||
<WorkspaceParametersPageViewExperimental
|
||||
templateVersionId={templateVersionId}
|
||||
workspace={workspace}
|
||||
autofillParameters={autofillParameters}
|
||||
canChangeVersions={canChangeVersions}
|
||||
|
||||
+25
-2
@@ -5,6 +5,7 @@ import type {
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Label } from "components/Label/Label";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
@@ -30,6 +31,7 @@ type WorkspaceParametersPageViewExperimentalProps = {
|
||||
rich_parameter_values: WorkspaceBuildParameter[];
|
||||
}) => void;
|
||||
sendMessage: (formValues: Record<string, string>) => 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<
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(templateVersionId || workspace.latest_build.template_version_id) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm text-content-secondary">Version ID</Label>
|
||||
<p className="m-0 text-sm font-medium">
|
||||
{templateVersionId ?? workspace.latest_build.template_version_id}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.handleSubmit} className="flex flex-col gap-8">
|
||||
{standardParameters.length > 0 && (
|
||||
<section className="flex flex-col gap-9">
|
||||
@@ -236,10 +248,21 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled || !form.dirty}
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
disabled ||
|
||||
diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
) ||
|
||||
parameters.some((parameter) =>
|
||||
parameter.diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Spinner loading={isSubmitting} />
|
||||
Submit and restart
|
||||
Update and restart
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user