Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 305da20064 | |||
| 5750dbbbf2 | |||
| e2bc48f61e | |||
| b5cb947c89 | |||
| da442c1a5e | |||
| 242a034b7b | |||
| 41ea7a8fa0 | |||
| 6a9921d495 | |||
| 77303f91df |
+3
-1
@@ -33,6 +33,7 @@ interface WorkspaceParameterFormProps {
|
||||
templateVersionRichParameters: TemplateVersionParameter[];
|
||||
autofillParams: AutofillBuildParameter[];
|
||||
isSubmitting: boolean;
|
||||
isInTransition: boolean;
|
||||
canChangeVersions: boolean;
|
||||
templatePermissions: { canUpdateTemplate: boolean } | undefined;
|
||||
error: unknown;
|
||||
@@ -50,6 +51,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
canChangeVersions,
|
||||
templatePermissions,
|
||||
isSubmitting,
|
||||
isInTransition,
|
||||
}) => {
|
||||
const form = useFormik<WorkspaceParametersFormValues>({
|
||||
onSubmit,
|
||||
@@ -169,7 +171,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || disabled || !form.dirty}
|
||||
disabled={isSubmitting || disabled || isInTransition || !form.dirty}
|
||||
>
|
||||
<Spinner loading={isSubmitting} />
|
||||
Submit and restart
|
||||
|
||||
+139
@@ -34,6 +34,14 @@ test("Submit the workspace settings page successfully", async () => {
|
||||
// Immutable value
|
||||
MockWorkspaceBuildParameter4,
|
||||
]);
|
||||
// Mock the API calls for stopping and restarting the workspace
|
||||
const stopWorkspaceSpy = vi
|
||||
.spyOn(API, "stopWorkspace")
|
||||
.mockResolvedValue({ ...MockWorkspaceBuild, transition: "stop" });
|
||||
const waitForBuildSpy = vi.spyOn(API, "waitForBuild").mockResolvedValue({
|
||||
...MockWorkspaceBuild.job,
|
||||
status: "succeeded",
|
||||
});
|
||||
// Mock the API calls that submit data
|
||||
const postWorkspaceBuildSpy = vi
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
@@ -66,6 +74,10 @@ test("Submit the workspace settings page successfully", async () => {
|
||||
);
|
||||
// Assert that the API calls were made with the correct data
|
||||
await waitFor(() => {
|
||||
// Since workspace is running, it should stop first
|
||||
expect(stopWorkspaceSpy).toHaveBeenCalledWith(MockWorkspace.id);
|
||||
expect(waitForBuildSpy).toHaveBeenCalled();
|
||||
// Then start with new parameters
|
||||
expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||
reason: "dashboard",
|
||||
transition: "start",
|
||||
@@ -77,6 +89,83 @@ test("Submit the workspace settings page successfully", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Submit the workspace settings page when workspace is stopped", async () => {
|
||||
// Create a stopped workspace
|
||||
const stoppedWorkspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspaceBuild,
|
||||
status: "stopped" as const,
|
||||
transition: "stop" as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the API calls that loads data
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce(
|
||||
stoppedWorkspace,
|
||||
);
|
||||
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
// Immutable parameters
|
||||
MockTemplateVersionParameter4,
|
||||
]);
|
||||
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
// Immutable value
|
||||
MockWorkspaceBuildParameter4,
|
||||
]);
|
||||
// Mock the API calls - stopWorkspace should NOT be called for stopped workspace
|
||||
const stopWorkspaceSpy = vi.spyOn(API, "stopWorkspace");
|
||||
const waitForBuildSpy = vi.spyOn(API, "waitForBuild");
|
||||
// Mock the API calls that submit data
|
||||
const postWorkspaceBuildSpy = vi
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
// Setup event and rendering
|
||||
const user = userEvent.setup();
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceParametersPage />, {
|
||||
route: "/@test-user/test-workspace/settings",
|
||||
path: "/:username/:workspace/settings",
|
||||
// Need this because after submit the user is redirected
|
||||
extraRoutes: [{ path: "/:username/:workspace", element: <div /> }],
|
||||
});
|
||||
await waitForLoaderToBeRemoved();
|
||||
// Fill the form and submit
|
||||
const form = screen.getByTestId("form");
|
||||
const parameter1 = within(form).getByLabelText(
|
||||
MockWorkspaceBuildParameter1.name,
|
||||
{ exact: false },
|
||||
);
|
||||
await user.clear(parameter1);
|
||||
await user.type(parameter1, "new-value");
|
||||
const parameter2 = within(form).getByLabelText(
|
||||
MockWorkspaceBuildParameter2.name,
|
||||
{ exact: false },
|
||||
);
|
||||
await user.clear(parameter2);
|
||||
await user.type(parameter2, "3");
|
||||
await user.click(
|
||||
within(form).getByRole("button", { name: "Submit and restart" }),
|
||||
);
|
||||
// Assert that the API calls were made with the correct data
|
||||
await waitFor(() => {
|
||||
// Since workspace is stopped, it should NOT stop first
|
||||
expect(stopWorkspaceSpy).not.toHaveBeenCalled();
|
||||
expect(waitForBuildSpy).not.toHaveBeenCalled();
|
||||
// Should just start with new parameters
|
||||
expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(stoppedWorkspace.id, {
|
||||
reason: "dashboard",
|
||||
transition: "start",
|
||||
rich_parameter_values: [
|
||||
{ name: MockTemplateVersionParameter1.name, value: "new-value" },
|
||||
{ name: MockTemplateVersionParameter2.name, value: "3" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Submit button is only enabled when changes are made", async () => {
|
||||
// Mock the API calls that loads data
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce(
|
||||
@@ -131,3 +220,53 @@ test("Submit button is only enabled when changes are made", async () => {
|
||||
// There are now no changes, the button should be disabled.
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Submit button is disabled and shows warning when workspace is in transition", async () => {
|
||||
// Create a workspace in starting state
|
||||
const startingWorkspace = {
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspaceBuild,
|
||||
status: "starting" as const,
|
||||
transition: "start" as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the API calls that loads data
|
||||
vi.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValueOnce(
|
||||
startingWorkspace,
|
||||
);
|
||||
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
]);
|
||||
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([
|
||||
MockWorkspaceBuildParameter1,
|
||||
MockWorkspaceBuildParameter2,
|
||||
]);
|
||||
|
||||
// Setup event and rendering
|
||||
const user = userEvent.setup();
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceParametersPage />, {
|
||||
route: "/@test-user/test-workspace/settings",
|
||||
path: "/:username/:workspace/settings",
|
||||
});
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const submitButton: HTMLButtonElement = screen.getByRole("button", {
|
||||
name: "Submit and restart",
|
||||
});
|
||||
|
||||
const form = screen.getByTestId("form");
|
||||
const parameter1 = within(form).getByLabelText(
|
||||
MockWorkspaceBuildParameter1.name,
|
||||
{ exact: false },
|
||||
);
|
||||
|
||||
// Make changes to the form
|
||||
await user.clear(parameter1);
|
||||
await user.type(parameter1, "new-value");
|
||||
|
||||
// Even with changes, button should still be disabled due to transition
|
||||
expect(submitButton.disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
+46
-3
@@ -8,6 +8,7 @@ import type {
|
||||
Workspace,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
@@ -39,12 +40,33 @@ const WorkspaceParametersPage: FC = () => {
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const updateParameters = useMutation({
|
||||
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
|
||||
API.postWorkspaceBuild(workspace.id, {
|
||||
mutationFn: async (buildParameters: WorkspaceBuildParameter[]) => {
|
||||
const currentBuild = workspace.latest_build;
|
||||
|
||||
// If workspace is running, stop it first then start with new parameters
|
||||
if (currentBuild.status === "running") {
|
||||
const stopBuild = await API.stopWorkspace(workspace.id);
|
||||
const awaitedStopBuild = await API.waitForBuild(stopBuild);
|
||||
// If the stop is canceled or failed, bail out
|
||||
if (awaitedStopBuild?.status === "canceled") {
|
||||
throw new Error(
|
||||
"Workspace stop was canceled, not proceeding with parameter update.",
|
||||
);
|
||||
}
|
||||
if (awaitedStopBuild?.status === "failed") {
|
||||
throw new Error(
|
||||
"Workspace failed to stop, not proceeding with parameter update.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now start the workspace with new parameters
|
||||
return API.postWorkspaceBuild(workspace.id, {
|
||||
transition: "start",
|
||||
rich_parameter_values: buildParameters,
|
||||
reason: "dashboard",
|
||||
}),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate(`/${workspace.owner_name}/${workspace.name}`);
|
||||
},
|
||||
@@ -78,6 +100,14 @@ const WorkspaceParametersPage: FC = () => {
|
||||
| { canUpdateTemplate: boolean }
|
||||
| undefined;
|
||||
|
||||
// Check if workspace is in a transitional state
|
||||
const isInTransition =
|
||||
workspace.latest_build.status === "starting" ||
|
||||
workspace.latest_build.status === "stopping" ||
|
||||
workspace.latest_build.status === "pending" ||
|
||||
workspace.latest_build.status === "canceling" ||
|
||||
workspace.latest_build.status === "deleting";
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(workspace.name, "Parameters")}</title>
|
||||
@@ -88,6 +118,7 @@ const WorkspaceParametersPage: FC = () => {
|
||||
buildParameters={buildParameters}
|
||||
canChangeVersions={canChangeVersions}
|
||||
templatePermissions={templatePermissions}
|
||||
isInTransition={isInTransition}
|
||||
submitError={updateParameters.error}
|
||||
isSubmitting={updateParameters.isPending}
|
||||
onSubmit={(values) => {
|
||||
@@ -123,6 +154,7 @@ type WorkspaceParametersPageViewProps = {
|
||||
templatePermissions: { canUpdateTemplate: boolean } | undefined;
|
||||
templateVersionParameters?: TemplateVersionParameter[];
|
||||
buildParameters?: WorkspaceBuildParameter[];
|
||||
isInTransition: boolean;
|
||||
submitError: unknown;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: (formValues: WorkspaceParametersFormValues) => void;
|
||||
@@ -137,6 +169,7 @@ export const WorkspaceParametersPageView: FC<
|
||||
templatePermissions,
|
||||
templateVersionParameters,
|
||||
buildParameters,
|
||||
isInTransition,
|
||||
submitError,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
@@ -154,12 +187,22 @@ export const WorkspaceParametersPageView: FC<
|
||||
<ErrorAlert error={submitError} css={{ marginBottom: 48 }} />
|
||||
) : null}
|
||||
|
||||
{isInTransition && (
|
||||
<Alert severity="info">
|
||||
There is currently a{" "}
|
||||
<strong>{workspace.latest_build.transition}</strong> workspace build{" "}
|
||||
<strong>{workspace.latest_build.status}</strong>. Please wait for the
|
||||
workspace build to complete before proceeding.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{templateVersionParameters && buildParameters ? (
|
||||
templateVersionParameters.length > 0 ? (
|
||||
<WorkspaceParametersForm
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
templatePermissions={templatePermissions}
|
||||
isInTransition={isInTransition}
|
||||
autofillParams={buildParameters.map((p) => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
|
||||
+43
-3
@@ -6,6 +6,7 @@ import type {
|
||||
DynamicParametersResponse,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { Link } from "components/Link/Link";
|
||||
@@ -143,13 +144,34 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
]);
|
||||
|
||||
const updateParameters = useMutation({
|
||||
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>
|
||||
API.postWorkspaceBuild(workspace.id, {
|
||||
mutationFn: async (buildParameters: WorkspaceBuildParameter[]) => {
|
||||
const currentBuild = workspace.latest_build;
|
||||
|
||||
// If workspace is running, stop it first then start with new parameters
|
||||
if (currentBuild.status === "running") {
|
||||
const stopBuild = await API.stopWorkspace(workspace.id);
|
||||
const awaitedStopBuild = await API.waitForBuild(stopBuild);
|
||||
// If the stop is canceled or failed, bail out
|
||||
if (awaitedStopBuild?.status === "canceled") {
|
||||
throw new Error(
|
||||
"Workspace stop was canceled, not proceeding with parameter update.",
|
||||
);
|
||||
}
|
||||
if (awaitedStopBuild?.status === "failed") {
|
||||
throw new Error(
|
||||
"Workspace failed to stop, not proceeding with parameter update.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now start the workspace with new parameters
|
||||
return API.postWorkspaceBuild(workspace.id, {
|
||||
transition: "start",
|
||||
template_version_id: templateVersionId,
|
||||
rich_parameter_values: buildParameters,
|
||||
reason: "dashboard",
|
||||
}),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate(`/@${workspace.owner_name}/${workspace.name}`);
|
||||
},
|
||||
@@ -195,6 +217,14 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
|
||||
const error = wsError || updateParameters.error;
|
||||
|
||||
// Check if workspace is in a transitional state
|
||||
const isInTransition =
|
||||
workspace.latest_build.status === "starting" ||
|
||||
workspace.latest_build.status === "stopping" ||
|
||||
workspace.latest_build.status === "pending" ||
|
||||
workspace.latest_build.status === "canceling" ||
|
||||
workspace.latest_build.status === "deleting";
|
||||
|
||||
if (
|
||||
latestBuildParametersLoading ||
|
||||
!latestResponse ||
|
||||
@@ -237,12 +267,22 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
|
||||
{isInTransition && (
|
||||
<Alert severity="info">
|
||||
There is currently a{" "}
|
||||
<strong>{workspace.latest_build.transition}</strong> workspace build{" "}
|
||||
<strong>{workspace.latest_build.status}</strong>. Please wait for the
|
||||
workspace build to complete before proceeding.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{sortedParams.length > 0 ? (
|
||||
<WorkspaceParametersPageViewExperimental
|
||||
templateVersionId={templateVersionId}
|
||||
workspace={workspace}
|
||||
autofillParameters={autofillParameters}
|
||||
canChangeVersions={canChangeVersions}
|
||||
isInTransition={isInTransition}
|
||||
parameters={sortedParams}
|
||||
diagnostics={latestResponse.diagnostics}
|
||||
isSubmitting={updateParameters.isPending}
|
||||
|
||||
+3
@@ -26,6 +26,7 @@ type WorkspaceParametersPageViewExperimentalProps = {
|
||||
diagnostics: PreviewParameter["diagnostics"];
|
||||
canChangeVersions: boolean;
|
||||
isSubmitting: boolean;
|
||||
isInTransition: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: {
|
||||
rich_parameter_values: WorkspaceBuildParameter[];
|
||||
@@ -43,6 +44,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
diagnostics,
|
||||
canChangeVersions,
|
||||
isSubmitting,
|
||||
isInTransition,
|
||||
onSubmit,
|
||||
sendMessage,
|
||||
onCancel,
|
||||
@@ -248,6 +250,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
disabled ||
|
||||
isInTransition ||
|
||||
diagnostics.some(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
) ||
|
||||
|
||||
@@ -36,7 +36,13 @@ export const WorkspaceSettingsLayout: FC = () => {
|
||||
error,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery(workspaceByOwnerAndName(username, workspaceName));
|
||||
} = useQuery({
|
||||
...workspaceByOwnerAndName(username, workspaceName),
|
||||
// Always refetch workspace data when navigating to settings pages to ensure
|
||||
// we have the latest build status. This prevents the isInTransition check
|
||||
// from using stale data and incorrectly disabling the submit button.
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
|
||||
const sharingSettingsQuery = useQuery({
|
||||
...workspaceSharingSettings(workspace?.organization_id ?? ""),
|
||||
|
||||
Reference in New Issue
Block a user