mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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:
co-authored by
blink-so[bot]
parent
c339066be3
commit
508fba83dd
+82
-10
@@ -24,7 +24,10 @@ import type dayjs from "dayjs";
|
|||||||
import userAgentParser from "ua-parser-js";
|
import userAgentParser from "ua-parser-js";
|
||||||
import { OneWayWebSocket } from "../utils/OneWayWebSocket";
|
import { OneWayWebSocket } from "../utils/OneWayWebSocket";
|
||||||
import { delay } from "../utils/delay";
|
import { delay } from "../utils/delay";
|
||||||
import type { PostWorkspaceUsageRequest } from "./typesGenerated";
|
import type {
|
||||||
|
DynamicParametersRequest,
|
||||||
|
PostWorkspaceUsageRequest,
|
||||||
|
} from "./typesGenerated";
|
||||||
import * as TypesGen from "./typesGenerated";
|
import * as TypesGen from "./typesGenerated";
|
||||||
|
|
||||||
const getMissingParameters = (
|
const getMissingParameters = (
|
||||||
@@ -73,8 +76,10 @@ const getMissingParameters = (
|
|||||||
if (templateParameter.options.length === 0) {
|
if (templateParameter.options.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// For multi-select, extra steps are necessary to JSON parse the value.
|
||||||
// Check if there is a new value
|
if (templateParameter.form_type === "multi-select") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let buildParameter = newBuildParameters.find(
|
let buildParameter = newBuildParameters.find(
|
||||||
(p) => p.name === templateParameter.name,
|
(p) => p.name === templateParameter.name,
|
||||||
);
|
);
|
||||||
@@ -231,7 +236,7 @@ export const watchWorkspaceAgentLogs = (
|
|||||||
/**
|
/**
|
||||||
* WebSocket compression in Safari (confirmed in 16.5) is broken when
|
* WebSocket compression in Safari (confirmed in 16.5) is broken when
|
||||||
* the server sends large messages. The following error is seen:
|
* 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") {
|
if (userAgentParser(navigator.userAgent).browser.name === "Safari") {
|
||||||
searchParams.set("no_compression", "");
|
searchParams.set("no_compression", "");
|
||||||
@@ -990,6 +995,17 @@ class ApiMethods {
|
|||||||
return response.data;
|
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 (
|
getTemplateVersionRichParameters = async (
|
||||||
versionId: string,
|
versionId: string,
|
||||||
): Promise<TypesGen.TemplateVersionParameter[]> => {
|
): Promise<TypesGen.TemplateVersionParameter[]> => {
|
||||||
@@ -2132,6 +2148,38 @@ class ApiMethods {
|
|||||||
await this.axios.delete(`/api/v2/licenses/${licenseId}`);
|
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
|
/** Steps to change the workspace version
|
||||||
* - Get the latest template to access the latest active version
|
* - Get the latest template to access the latest active version
|
||||||
* - Get the current build parameters
|
* - Get the current build parameters
|
||||||
@@ -2145,11 +2193,23 @@ class ApiMethods {
|
|||||||
workspace: TypesGen.Workspace,
|
workspace: TypesGen.Workspace,
|
||||||
templateVersionId: string,
|
templateVersionId: string,
|
||||||
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
|
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
|
||||||
|
isDynamicParametersEnabled = false,
|
||||||
): Promise<TypesGen.WorkspaceBuild> => {
|
): Promise<TypesGen.WorkspaceBuild> => {
|
||||||
const [currentBuildParameters, templateParameters] = await Promise.all([
|
const currentBuildParameters = await this.getWorkspaceBuildParameters(
|
||||||
this.getWorkspaceBuildParameters(workspace.latest_build.id),
|
workspace.latest_build.id,
|
||||||
this.getTemplateVersionRichParameters(templateVersionId),
|
);
|
||||||
]);
|
|
||||||
|
let templateParameters: TypesGen.TemplateVersionParameter[] = [];
|
||||||
|
if (isDynamicParametersEnabled) {
|
||||||
|
templateParameters = await this.getDynamicParameters(
|
||||||
|
templateVersionId,
|
||||||
|
workspace.owner_id,
|
||||||
|
currentBuildParameters,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
templateParameters =
|
||||||
|
await this.getTemplateVersionRichParameters(templateVersionId);
|
||||||
|
}
|
||||||
|
|
||||||
const missingParameters = getMissingParameters(
|
const missingParameters = getMissingParameters(
|
||||||
currentBuildParameters,
|
currentBuildParameters,
|
||||||
@@ -2180,6 +2240,7 @@ class ApiMethods {
|
|||||||
updateWorkspace = async (
|
updateWorkspace = async (
|
||||||
workspace: TypesGen.Workspace,
|
workspace: TypesGen.Workspace,
|
||||||
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
|
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
|
||||||
|
isDynamicParametersEnabled = false,
|
||||||
): Promise<TypesGen.WorkspaceBuild> => {
|
): Promise<TypesGen.WorkspaceBuild> => {
|
||||||
const [template, oldBuildParameters] = await Promise.all([
|
const [template, oldBuildParameters] = await Promise.all([
|
||||||
this.getTemplate(workspace.template_id),
|
this.getTemplate(workspace.template_id),
|
||||||
@@ -2187,8 +2248,19 @@ class ApiMethods {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const activeVersionId = template.active_version_id;
|
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(
|
const missingParameters = getMissingParameters(
|
||||||
oldBuildParameters,
|
oldBuildParameters,
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export const updateDeadline = (
|
|||||||
export const changeVersion = (
|
export const changeVersion = (
|
||||||
workspace: Workspace,
|
workspace: Workspace,
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
|
isDynamicParametersEnabled: boolean,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
mutationFn: ({
|
mutationFn: ({
|
||||||
@@ -172,7 +173,12 @@ export const changeVersion = (
|
|||||||
versionId: string;
|
versionId: string;
|
||||||
buildParameters?: WorkspaceBuildParameter[];
|
buildParameters?: WorkspaceBuildParameter[];
|
||||||
}) => {
|
}) => {
|
||||||
return API.changeWorkspaceVersion(workspace, versionId, buildParameters);
|
return API.changeWorkspaceVersion(
|
||||||
|
workspace,
|
||||||
|
versionId,
|
||||||
|
buildParameters,
|
||||||
|
isDynamicParametersEnabled,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: async (build: WorkspaceBuild) => {
|
onSuccess: async (build: WorkspaceBuild) => {
|
||||||
await updateWorkspaceBuild(build, queryClient);
|
await updateWorkspaceBuild(build, queryClient);
|
||||||
@@ -185,8 +191,18 @@ export const updateWorkspace = (
|
|||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
mutationFn: (buildParameters?: WorkspaceBuildParameter[]) => {
|
mutationFn: ({
|
||||||
return API.updateWorkspace(workspace, buildParameters);
|
buildParameters,
|
||||||
|
isDynamicParametersEnabled,
|
||||||
|
}: {
|
||||||
|
buildParameters?: WorkspaceBuildParameter[];
|
||||||
|
isDynamicParametersEnabled: boolean;
|
||||||
|
}) => {
|
||||||
|
return API.updateWorkspace(
|
||||||
|
workspace,
|
||||||
|
buildParameters,
|
||||||
|
isDynamicParametersEnabled,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onSuccess: async (build: WorkspaceBuild) => {
|
onSuccess: async (build: WorkspaceBuild) => {
|
||||||
await updateWorkspaceBuild(build, queryClient);
|
await updateWorkspaceBuild(build, queryClient);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const DialogContent = forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
|
||||||
translate-x-[-50%] translate-y-[-50%]
|
translate-x-[-50%] translate-y-[-50%]
|
||||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||||
@@ -68,7 +68,7 @@ export const DialogHeader: FC<HTMLAttributes<HTMLDivElement>> = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -108,7 +108,7 @@ export const DialogDescription = forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-content-secondary", className)}
|
className={cn("text-sm text-content-secondary font-medium", className)}
|
||||||
{...props}
|
{...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,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
|
import { useDynamicParametersOptOut } from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut";
|
||||||
import { type FC, useEffect, useState } from "react";
|
import { type FC, useEffect, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog";
|
import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog";
|
||||||
import { DownloadLogsDialog } from "./DownloadLogsDialog";
|
import { DownloadLogsDialog } from "./DownloadLogsDialog";
|
||||||
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog";
|
||||||
|
import { UpdateBuildParametersDialogExperimental } from "./UpdateBuildParametersDialogExperimental";
|
||||||
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog";
|
||||||
import { useWorkspaceDuplication } from "./useWorkspaceDuplication";
|
import { useWorkspaceDuplication } from "./useWorkspaceDuplication";
|
||||||
|
|
||||||
@@ -40,6 +43,15 @@ export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
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
|
// Permissions
|
||||||
const { data: permissions } = useQuery(workspacePermissions(workspace));
|
const { data: permissions } = useQuery(workspacePermissions(workspace));
|
||||||
@@ -50,7 +62,7 @@ export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({
|
|||||||
// Change version
|
// Change version
|
||||||
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
||||||
const changeVersionMutation = useMutation(
|
const changeVersionMutation = useMutation(
|
||||||
changeVersion(workspace, queryClient),
|
changeVersion(workspace, queryClient, optOutQuery.data?.optedOut === false),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
@@ -142,25 +154,46 @@ export const WorkspaceMoreActions: FC<WorkspaceMoreActionsProps> = ({
|
|||||||
onClose={() => setIsDownloadDialogOpen(false)}
|
onClose={() => setIsDownloadDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UpdateBuildParametersDialog
|
{!isDynamicParametersEnabled || optOutQuery.data?.optedOut ? (
|
||||||
missedParameters={
|
<UpdateBuildParametersDialog
|
||||||
changeVersionMutation.error instanceof MissingBuildParameters
|
missedParameters={
|
||||||
? changeVersionMutation.error.parameters
|
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
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
|
<ChangeWorkspaceVersionDialog
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import type {
|
|||||||
WorkspaceBuild,
|
WorkspaceBuild,
|
||||||
WorkspaceBuildParameter,
|
WorkspaceBuildParameter,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
|
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||||
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
|
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 { UpdateBuildParametersDialog } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog";
|
||||||
|
import { UpdateBuildParametersDialogExperimental } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialogExperimental";
|
||||||
import { type FC, useState } from "react";
|
import { type FC, useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
|
||||||
@@ -51,8 +56,21 @@ export const useWorkspaceUpdate = ({
|
|||||||
setIsConfirmingUpdate(true);
|
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[] = []) => {
|
const confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => {
|
||||||
updateWorkspaceMutation.mutate(buildParameters);
|
updateWorkspaceMutation.mutate({
|
||||||
|
buildParameters,
|
||||||
|
isDynamicParametersEnabled: optOutQuery.data?.optedOut === false,
|
||||||
|
});
|
||||||
setIsConfirmingUpdate(false);
|
setIsConfirmingUpdate(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +85,7 @@ export const useWorkspaceUpdate = ({
|
|||||||
latestVersion,
|
latestVersion,
|
||||||
},
|
},
|
||||||
missingBuildParameters: {
|
missingBuildParameters: {
|
||||||
|
workspace,
|
||||||
error: updateWorkspaceMutation.error,
|
error: updateWorkspaceMutation.error,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
updateWorkspaceMutation.reset();
|
updateWorkspaceMutation.reset();
|
||||||
@@ -134,22 +153,57 @@ const UpdateConfirmationDialog: FC<UpdateConfirmationDialogProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
type MissingBuildParametersDialogProps = {
|
type MissingBuildParametersDialogProps = {
|
||||||
|
workspace: Workspace;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void;
|
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MissingBuildParametersDialog: FC<MissingBuildParametersDialogProps> = ({
|
const MissingBuildParametersDialog: FC<MissingBuildParametersDialogProps> = ({
|
||||||
|
workspace,
|
||||||
error,
|
error,
|
||||||
...dialogProps
|
...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
|
<UpdateBuildParametersDialog
|
||||||
missedParameters={
|
missedParameters={missedParameters}
|
||||||
error instanceof MissingBuildParameters ? error.parameters : []
|
open={isOpen}
|
||||||
}
|
|
||||||
open={error instanceof MissingBuildParameters}
|
|
||||||
{...dialogProps}
|
{...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 { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
|
import {
|
||||||
|
optOutKey,
|
||||||
|
useDynamicParametersOptOut,
|
||||||
|
} from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@@ -11,39 +15,26 @@ import { ExperimentalFormContext } from "./ExperimentalFormContext";
|
|||||||
|
|
||||||
const CreateWorkspaceExperimentRouter: FC = () => {
|
const CreateWorkspaceExperimentRouter: FC = () => {
|
||||||
const { experiments } = useDashboard();
|
const { experiments } = useDashboard();
|
||||||
const dynamicParametersEnabled = experiments.includes("dynamic-parameters");
|
const isDynamicParametersEnabled = experiments.includes("dynamic-parameters");
|
||||||
|
|
||||||
const { organization: organizationName = "default", template: templateName } =
|
const { organization: organizationName = "default", template: templateName } =
|
||||||
useParams() as { organization?: string; template: string };
|
useParams() as { organization?: string; template: string };
|
||||||
const templateQuery = useQuery({
|
const templateQuery = useQuery({
|
||||||
...templateByName(organizationName, templateName),
|
...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,
|
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) {
|
if (optOutQuery.isError) {
|
||||||
return <ErrorAlert error={optOutQuery.error} />;
|
return <ErrorAlert error={optOutQuery.error} />;
|
||||||
}
|
}
|
||||||
@@ -77,5 +68,3 @@ const CreateWorkspaceExperimentRouter: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default CreateWorkspaceExperimentRouter;
|
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
|
// Check if the update was called using the values from the form
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(API.updateWorkspace).toBeCalledWith(MockOutdatedWorkspace, [
|
expect(API.updateWorkspace).toBeCalledWith(
|
||||||
{
|
MockOutdatedWorkspace,
|
||||||
name: MockTemplateVersionParameter1.name,
|
[
|
||||||
value: "some-value",
|
{
|
||||||
},
|
name: MockTemplateVersionParameter1.name,
|
||||||
{
|
value: "some-value",
|
||||||
name: MockTemplateVersionParameter2.name,
|
},
|
||||||
value: "2",
|
{
|
||||||
},
|
name: MockTemplateVersionParameter2.name,
|
||||||
]);
|
value: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
-37
@@ -1,8 +1,11 @@
|
|||||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
|
import {
|
||||||
|
optOutKey,
|
||||||
|
useDynamicParametersOptOut,
|
||||||
|
} from "modules/workspaces/DynamicParameter/useDynamicParametersOptOut";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useQuery } from "react-query";
|
|
||||||
import { ExperimentalFormContext } from "../../CreateWorkspacePage/ExperimentalFormContext";
|
import { ExperimentalFormContext } from "../../CreateWorkspacePage/ExperimentalFormContext";
|
||||||
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
|
import { useWorkspaceSettings } from "../WorkspaceSettingsLayout";
|
||||||
import WorkspaceParametersPage from "./WorkspaceParametersPage";
|
import WorkspaceParametersPage from "./WorkspaceParametersPage";
|
||||||
@@ -11,45 +14,21 @@ import WorkspaceParametersPageExperimental from "./WorkspaceParametersPageExperi
|
|||||||
const WorkspaceParametersExperimentRouter: FC = () => {
|
const WorkspaceParametersExperimentRouter: FC = () => {
|
||||||
const { experiments } = useDashboard();
|
const { experiments } = useDashboard();
|
||||||
const workspace = useWorkspaceSettings();
|
const workspace = useWorkspaceSettings();
|
||||||
const dynamicParametersEnabled = experiments.includes("dynamic-parameters");
|
const isDynamicParametersEnabled = experiments.includes("dynamic-parameters");
|
||||||
|
|
||||||
const optOutQuery = useQuery({
|
const optOutQuery = useDynamicParametersOptOut({
|
||||||
enabled: dynamicParametersEnabled,
|
templateId: workspace.template_id,
|
||||||
queryKey: [
|
templateUsesClassicParameters:
|
||||||
"workspace",
|
workspace.template_use_classic_parameter_flow,
|
||||||
workspace.id,
|
enabled: isDynamicParametersEnabled,
|
||||||
"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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dynamicParametersEnabled) {
|
if (isDynamicParametersEnabled) {
|
||||||
if (optOutQuery.isLoading) {
|
if (optOutQuery.isError) {
|
||||||
return <Loader />;
|
return <ErrorAlert error={optOutQuery.error} />;
|
||||||
}
|
}
|
||||||
if (!optOutQuery.data) {
|
if (!optOutQuery.data) {
|
||||||
return <ErrorAlert error={optOutQuery.error} />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleOptedOut = () => {
|
const toggleOptedOut = () => {
|
||||||
@@ -79,5 +58,3 @@ const WorkspaceParametersExperimentRouter: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default WorkspaceParametersExperimentRouter;
|
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 { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useMutation, useQuery } from "react-query";
|
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 { docs } from "utils/docs";
|
||||||
import { pageTitle } from "utils/page";
|
import { pageTitle } from "utils/page";
|
||||||
import type { AutofillBuildParameter } from "utils/richParameters";
|
import type { AutofillBuildParameter } from "utils/richParameters";
|
||||||
@@ -40,6 +40,8 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
const workspace = useWorkspaceSettings();
|
const workspace = useWorkspaceSettings();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const experimentalFormContext = useContext(ExperimentalFormContext);
|
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
|
// autofill the form with the workspace build parameters from the latest build
|
||||||
const {
|
const {
|
||||||
@@ -107,10 +109,11 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspace.latest_build.template_version_id) return;
|
if (!templateVersionId && !workspace.latest_build.template_version_id)
|
||||||
|
return;
|
||||||
|
|
||||||
const socket = API.templateVersionDynamicParameters(
|
const socket = API.templateVersionDynamicParameters(
|
||||||
workspace.latest_build.template_version_id,
|
templateVersionId ?? workspace.latest_build.template_version_id,
|
||||||
{
|
{
|
||||||
onMessage,
|
onMessage,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -136,12 +139,17 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
socket.close();
|
socket.close();
|
||||||
};
|
};
|
||||||
}, [workspace.latest_build.template_version_id, onMessage]);
|
}, [
|
||||||
|
templateVersionId,
|
||||||
|
workspace.latest_build.template_version_id,
|
||||||
|
onMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
const updateParameters = useMutation({
|
const updateParameters = useMutation({
|
||||||
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
|
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
|
||||||
API.postWorkspaceBuild(workspace.id, {
|
API.postWorkspaceBuild(workspace.id, {
|
||||||
transition: "start",
|
transition: "start",
|
||||||
|
template_version_id: templateVersionId,
|
||||||
rich_parameter_values: buildParameters,
|
rich_parameter_values: buildParameters,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -250,6 +258,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
|||||||
|
|
||||||
{sortedParams.length > 0 ? (
|
{sortedParams.length > 0 ? (
|
||||||
<WorkspaceParametersPageViewExperimental
|
<WorkspaceParametersPageViewExperimental
|
||||||
|
templateVersionId={templateVersionId}
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
autofillParameters={autofillParameters}
|
autofillParameters={autofillParameters}
|
||||||
canChangeVersions={canChangeVersions}
|
canChangeVersions={canChangeVersions}
|
||||||
|
|||||||
+25
-2
@@ -5,6 +5,7 @@ import type {
|
|||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
import { Alert } from "components/Alert/Alert";
|
import { Alert } from "components/Alert/Alert";
|
||||||
import { Button } from "components/Button/Button";
|
import { Button } from "components/Button/Button";
|
||||||
|
import { Label } from "components/Label/Label";
|
||||||
import { Link } from "components/Link/Link";
|
import { Link } from "components/Link/Link";
|
||||||
import { Spinner } from "components/Spinner/Spinner";
|
import { Spinner } from "components/Spinner/Spinner";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -30,6 +31,7 @@ type WorkspaceParametersPageViewExperimentalProps = {
|
|||||||
rich_parameter_values: WorkspaceBuildParameter[];
|
rich_parameter_values: WorkspaceBuildParameter[];
|
||||||
}) => void;
|
}) => void;
|
||||||
sendMessage: (formValues: Record<string, string>) => void;
|
sendMessage: (formValues: Record<string, string>) => void;
|
||||||
|
templateVersionId: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceParametersPageViewExperimental: FC<
|
export const WorkspaceParametersPageViewExperimental: FC<
|
||||||
@@ -44,6 +46,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
templateVersionId,
|
||||||
}) => {
|
}) => {
|
||||||
const autofillByName = Object.fromEntries(
|
const autofillByName = Object.fromEntries(
|
||||||
autofillParameters.map((param) => [param.name, param]),
|
autofillParameters.map((param) => [param.name, param]),
|
||||||
@@ -152,6 +155,15 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
|||||||
</div>
|
</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">
|
<form onSubmit={form.handleSubmit} className="flex flex-col gap-8">
|
||||||
{standardParameters.length > 0 && (
|
{standardParameters.length > 0 && (
|
||||||
<section className="flex flex-col gap-9">
|
<section className="flex flex-col gap-9">
|
||||||
@@ -236,10 +248,21 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
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} />
|
<Spinner loading={isSubmitting} />
|
||||||
Submit and restart
|
Update and restart
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ describe("WorkspacesPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateWorkspace).toHaveBeenCalledTimes(2);
|
expect(updateWorkspace).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3], [], false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warns about and updates running workspaces", async () => {
|
it("warns about and updates running workspaces", async () => {
|
||||||
@@ -160,9 +160,9 @@ describe("WorkspacesPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateWorkspace).toHaveBeenCalledTimes(3);
|
expect(updateWorkspace).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warns about and ignores dormant workspaces", async () => {
|
it("warns about and ignores dormant workspaces", async () => {
|
||||||
@@ -199,8 +199,8 @@ describe("WorkspacesPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateWorkspace).toHaveBeenCalledTimes(2);
|
expect(updateWorkspace).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[1], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warns about running workspaces and then dormant workspaces", async () => {
|
it("warns about running workspaces and then dormant workspaces", async () => {
|
||||||
@@ -241,9 +241,9 @@ describe("WorkspacesPage", () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateWorkspace).toHaveBeenCalledTimes(3);
|
expect(updateWorkspace).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[0], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[2], [], false);
|
||||||
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3]);
|
expect(updateWorkspace).toHaveBeenCalledWith(workspaces[3], [], false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ function useSafeSearchParams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WorkspacesPage: FC = () => {
|
const WorkspacesPage: FC = () => {
|
||||||
|
const { experiments } = useDashboard();
|
||||||
|
const isDynamicParametersEnabled = experiments.includes("dynamic-parameters");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
// If we use a useSearchParams for each hook, the values will not be in sync.
|
// 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
|
// So we have to use a single one, centralizing the values, and pass it to
|
||||||
@@ -162,7 +164,10 @@ const WorkspacesPage: FC = () => {
|
|||||||
checkedWorkspaces={checkedWorkspaces}
|
checkedWorkspaces={checkedWorkspaces}
|
||||||
open={confirmingBatchAction === "update"}
|
open={confirmingBatchAction === "update"}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
await batchActions.updateAll(checkedWorkspaces);
|
await batchActions.updateAll({
|
||||||
|
workspaces: checkedWorkspaces,
|
||||||
|
isDynamicParametersEnabled,
|
||||||
|
});
|
||||||
setConfirmingBatchAction(null);
|
setConfirmingBatchAction(null);
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
@@ -45,11 +45,15 @@ export function useBatchActions(options: UseBatchActionsProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateAllMutation = useMutation({
|
const updateAllMutation = useMutation({
|
||||||
mutationFn: (workspaces: readonly Workspace[]) => {
|
mutationFn: (payload: {
|
||||||
|
workspaces: readonly Workspace[];
|
||||||
|
isDynamicParametersEnabled: boolean;
|
||||||
|
}) => {
|
||||||
|
const { workspaces, isDynamicParametersEnabled } = payload;
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
workspaces
|
workspaces
|
||||||
.filter((w) => w.outdated && !w.dormant_at)
|
.filter((w) => w.outdated && !w.dormant_at)
|
||||||
.map((w) => API.updateWorkspace(w)),
|
.map((w) => API.updateWorkspace(w, [], isDynamicParametersEnabled)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
|||||||
Reference in New Issue
Block a user