refactor: get rid of `templateVariablesXService` (#9763)

This commit is contained in:
Kayla Washburn 2023-09-19 11:54:14 -06:00 committed by GitHub
parent 530dd9d247
commit 269b1c59f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 362 deletions

View File

@ -61,7 +61,15 @@ export const mapApiErrorToFieldErrors = (
export const getErrorMessage = (
error: unknown,
defaultMessage: string,
): string => (isApiError(error) ? error.response.data.message : defaultMessage);
): string => {
if (isApiError(error)) {
return error.response.data.message;
}
if (typeof error === "string") {
return error;
}
return defaultMessage;
};
/**
*

View File

@ -1,6 +1,13 @@
import * as API from "api/api";
import { type Template, type AuthorizationResponse } from "api/typesGenerated";
import { type QueryOptions } from "@tanstack/react-query";
import {
type Template,
type AuthorizationResponse,
type CreateTemplateVersionRequest,
type ProvisionerJobStatus,
type TemplateVersion,
} from "api/typesGenerated";
import { type QueryClient, type QueryOptions } from "@tanstack/react-query";
import { delay } from "utils/delay";
export const templateByNameKey = (orgId: string, name: string) => [
orgId,
@ -63,3 +70,53 @@ export const templateVersions = (templateId: string) => {
queryFn: () => API.getTemplateVersions(templateId),
};
};
export const templateVersionVariables = (versionId: string) => {
return {
queryKey: ["templateVersion", versionId, "variables"],
queryFn: () => API.getTemplateVersionVariables(versionId),
};
};
export const createAndBuildTemplateVersion = (orgId: string) => {
return {
mutationFn: async (
request: CreateTemplateVersionRequest,
): Promise<string> => {
const newVersion = await API.createTemplateVersion(orgId, request);
let data: TemplateVersion;
let jobStatus: ProvisionerJobStatus;
do {
await delay(1000);
data = await API.getTemplateVersion(newVersion.id);
jobStatus = data.job.status;
if (jobStatus === "succeeded") {
return newVersion.id;
}
} while (jobStatus === "pending" || jobStatus === "running");
// No longer pending/running, but didn't succeed
throw data.job.error;
},
};
};
export const updateActiveTemplateVersion = (
template: Template,
queryClient: QueryClient,
) => {
return {
mutationFn: (versionId: string) =>
API.updateActiveTemplateVersion(template.id, {
id: versionId,
}),
onSuccess: async () => {
// invalidated because of `active_version_id`
await queryClient.invalidateQueries(
templateByNameKey(template.organization_id, template.name),
);
},
};
};

View File

@ -13,16 +13,14 @@ import {
MockTemplateVersionVariable1,
MockTemplateVersionVariable2,
MockTemplateVersion2,
MockTemplateVersionVariable5,
} from "testHelpers/entities";
import { delay } from "utils/delay";
const validFormValues = {
first_variable: "Hello world",
second_variable: "123",
};
const validationRequiredField = "Variable is required.";
const renderTemplateVariablesPage = async () => {
renderWithTemplateSettingsLayout(<TemplateVariablesPage />, {
route: `/templates/${MockTemplate.name}/variables`,
@ -62,7 +60,7 @@ describe("TemplateVariablesPage", () => {
jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
jest
.spyOn(API, "getTemplateVersion")
.mockResolvedValueOnce(MockTemplateVersion);
.mockResolvedValue(MockTemplateVersion);
jest
.spyOn(API, "getTemplateVersionVariables")
.mockResolvedValueOnce([
@ -106,49 +104,9 @@ describe("TemplateVariablesPage", () => {
FooterFormLanguage.defaultSubmitLabel,
);
await userEvent.click(submitButton);
// Wait for the success message
await delay(1500);
await screen.findByText("Template updated successfully");
});
it("user forgets to fill the required field", async () => {
jest.spyOn(API, "getTemplateByName").mockResolvedValueOnce(MockTemplate);
jest
.spyOn(API, "getTemplateVersion")
.mockResolvedValueOnce(MockTemplateVersion);
jest
.spyOn(API, "getTemplateVersionVariables")
.mockResolvedValueOnce([
MockTemplateVersionVariable1,
MockTemplateVersionVariable5,
]);
jest
.spyOn(API, "createTemplateVersion")
.mockResolvedValueOnce(MockTemplateVersion2);
jest.spyOn(API, "updateActiveTemplateVersion").mockResolvedValueOnce({
message: "done",
});
await renderTemplateVariablesPage();
const firstVariable = await screen.findByLabelText(
MockTemplateVersionVariable1.name,
);
expect(firstVariable).toBeDefined();
const fifthVariable = await screen.findByLabelText(
MockTemplateVersionVariable5.name,
);
expect(fifthVariable).toBeDefined();
// Submit the form
const submitButton = await screen.findByText(
FooterFormLanguage.defaultSubmitLabel,
);
await userEvent.click(submitButton);
// Check validation error
const validationError = await screen.findByText(validationRequiredField);
expect(validationError).toBeDefined();
});
});

View File

@ -1,4 +1,3 @@
import { useMachine } from "@xstate/react";
import {
CreateTemplateVersionRequest,
TemplateVersionVariable,
@ -6,40 +5,84 @@ import {
} from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react";
import { useCallback, type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useNavigate, useParams } from "react-router-dom";
import { templateVariablesMachine } from "xServices/template/templateVariablesXService";
import { pageTitle } from "../../../utils/page";
import { pageTitle } from "utils/page";
import { useTemplateSettings } from "../TemplateSettingsLayout";
import { TemplateVariablesPageView } from "./TemplateVariablesPageView";
import {
createAndBuildTemplateVersion,
templateVersion,
templateVersionVariables,
updateActiveTemplateVersion,
} from "api/queries/templates";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Loader } from "components/Loader/Loader";
export const TemplateVariablesPage: FC = () => {
const { template: templateName } = useParams() as {
organization: string;
template: string;
};
const organizationId = useOrganizationId();
const orgId = useOrganizationId();
const { template } = useTemplateSettings();
const navigate = useNavigate();
const [state, send] = useMachine(templateVariablesMachine, {
context: {
organizationId,
template,
},
actions: {
onUpdateTemplate: () => {
displaySuccess("Template updated successfully");
},
},
});
const queryClient = useQueryClient();
const versionId = template.active_version_id;
const {
activeTemplateVersion,
templateVariables,
getTemplateDataError,
updateTemplateError,
jobError,
} = state.context;
data: version,
error: versionError,
isLoading: isVersionLoading,
} = useQuery({ ...templateVersion(versionId), keepPreviousData: true });
const {
data: variables,
error: variablesError,
isLoading: isVariablesLoading,
} = useQuery({
...templateVersionVariables(versionId),
keepPreviousData: true,
});
const {
mutateAsync: sendCreateAndBuildTemplateVersion,
error: buildError,
isLoading: isBuilding,
} = useMutation(createAndBuildTemplateVersion(orgId));
const {
mutateAsync: sendUpdateActiveTemplateVersion,
error: publishError,
isLoading: isPublishing,
} = useMutation(updateActiveTemplateVersion(template, queryClient));
const publishVersion = useCallback(
async (versionId: string) => {
await sendUpdateActiveTemplateVersion(versionId);
displaySuccess("Template updated successfully");
},
[sendUpdateActiveTemplateVersion],
);
const buildVersion = useCallback(
async (req: CreateTemplateVersionRequest) => {
const newVersionId = await sendCreateAndBuildTemplateVersion(req);
await publishVersion(newVersionId);
},
[sendCreateAndBuildTemplateVersion, publishVersion],
);
const isSubmitting = Boolean(isBuilding || isPublishing);
const error = versionError ?? variablesError;
if (error) {
return <ErrorAlert error={error} />;
}
if (isVersionLoading || isVariablesLoading) {
return <Loader />;
}
return (
<>
@ -48,23 +91,19 @@ export const TemplateVariablesPage: FC = () => {
</Helmet>
<TemplateVariablesPageView
isSubmitting={state.hasTag("submitting")}
templateVersion={activeTemplateVersion}
templateVariables={templateVariables}
isSubmitting={isSubmitting}
templateVersion={version}
templateVariables={variables}
errors={{
getTemplateDataError,
updateTemplateError,
jobError,
buildError,
publishError,
}}
onCancel={() => {
navigate(`/templates/${templateName}`);
}}
onSubmit={(formData) => {
const request = filterEmptySensitiveVariables(
formData,
templateVariables,
);
send({ type: "UPDATE_TEMPLATE_EVENT", request: request });
onSubmit={async (formData) => {
const request = filterEmptySensitiveVariables(formData, variables);
await buildVersion(request);
}}
/>
</>

View File

@ -49,7 +49,7 @@ export const RequiredVariable: Story = {
},
};
export const WithUpdateTemplateError: Story = {
export const WithErrors: Story = {
args: {
templateVersion: MockTemplateVersion,
templateVariables: [
@ -59,25 +59,20 @@ export const WithUpdateTemplateError: Story = {
MockTemplateVersionVariable4,
],
errors: {
updateTemplateError: mockApiError({
message: "Something went wrong.",
buildError: mockApiError({
message: "buildError",
validations: [
{
field: `user_variable_values[0].value`,
detail: "Variable is required.",
},
],
}),
publishError: mockApiError({ message: "publishError" }),
},
},
};
export const WithJobError: Story = {
args: {
templateVersion: MockTemplateVersion,
templateVariables: [
MockTemplateVersionVariable1,
MockTemplateVersionVariable2,
MockTemplateVersionVariable3,
MockTemplateVersionVariable4,
],
errors: {
jobError:
"template import provision for start: recv import provision: plan terraform: terraform plan: exit status 1",
initialTouched: {
user_variable_values: true,
},
},
};

View File

@ -4,12 +4,12 @@ import {
TemplateVersionVariable,
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { Loader } from "components/Loader/Loader";
import { ComponentProps, FC } from "react";
import { TemplateVariablesForm } from "./TemplateVariablesForm";
import { makeStyles } from "@mui/styles";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Stack } from "components/Stack/Stack";
export interface TemplateVariablesPageViewProps {
templateVersion?: TemplateVersion;
@ -18,9 +18,14 @@ export interface TemplateVariablesPageViewProps {
onCancel: () => void;
isSubmitting: boolean;
errors?: {
getTemplateDataError?: unknown;
updateTemplateError?: unknown;
jobError?: TemplateVersion["job"]["error"];
/**
* Failed to build a new template version
*/
buildError?: unknown;
/**
* New version was created successfully, but publishing it failed
*/
publishError?: unknown;
};
initialTouched?: ComponentProps<
typeof TemplateVariablesForm
@ -37,29 +42,23 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
initialTouched,
}) => {
const classes = useStyles();
const isLoading =
!templateVersion &&
!templateVariables &&
!errors.getTemplateDataError &&
!errors.updateTemplateError;
const hasError = Object.values(errors).some((error) => Boolean(error));
return (
<>
<PageHeader className={classes.pageHeader}>
<PageHeaderTitle>Template variables</PageHeaderTitle>
</PageHeader>
{hasError && (
<div className={classes.errorContainer}>
{Boolean(errors.getTemplateDataError) && (
<ErrorAlert error={errors.getTemplateDataError} />
<Stack className={classes.errorContainer}>
{Boolean(errors.buildError) && (
<ErrorAlert error={errors.buildError} />
)}
{Boolean(errors.updateTemplateError) && (
<ErrorAlert error={errors.updateTemplateError} />
{Boolean(errors.publishError) && (
<ErrorAlert error={errors.publishError} />
)}
{Boolean(errors.jobError) && <ErrorAlert error={errors.jobError} />}
</div>
</Stack>
)}
{isLoading && <Loader />}
{templateVersion && templateVariables && templateVariables.length > 0 && (
<TemplateVariablesForm
initialTouched={initialTouched}
@ -68,7 +67,7 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
templateVariables={templateVariables}
onSubmit={onSubmit}
onCancel={onCancel}
error={errors.updateTemplateError}
error={errors.buildError}
/>
)}
{templateVariables && templateVariables.length === 0 && (

View File

@ -1,244 +0,0 @@
import {
createTemplateVersion,
getTemplateVersion,
getTemplateVersionVariables,
updateActiveTemplateVersion,
} from "api/api";
import {
CreateTemplateVersionRequest,
Response,
Template,
TemplateVersion,
TemplateVersionVariable,
} from "api/typesGenerated";
import { assign, createMachine } from "xstate";
import { delay } from "utils/delay";
type TemplateVariablesContext = {
organizationId: string;
template: Template;
activeTemplateVersion?: TemplateVersion;
templateVariables?: TemplateVersionVariable[];
createTemplateVersionRequest?: CreateTemplateVersionRequest;
newTemplateVersion?: TemplateVersion;
getTemplateDataError?: unknown;
updateTemplateError?: unknown;
jobError?: TemplateVersion["job"]["error"];
};
type UpdateTemplateEvent = {
type: "UPDATE_TEMPLATE_EVENT";
request: CreateTemplateVersionRequest;
};
export const templateVariablesMachine = createMachine(
{
id: "templateVariablesState",
predictableActionArguments: true,
tsTypes: {} as import("./templateVariablesXService.typegen").Typegen0,
schema: {
context: {} as TemplateVariablesContext,
events: {} as UpdateTemplateEvent,
services: {} as {
getActiveTemplateVersion: {
data: TemplateVersion;
};
getTemplateVariables: {
data: TemplateVersionVariable[];
};
createNewTemplateVersion: {
data: TemplateVersion;
};
waitForJobToBeCompleted: {
data: TemplateVersion;
};
updateTemplate: {
data: Response;
};
},
},
initial: "gettingActiveTemplateVersion",
states: {
gettingActiveTemplateVersion: {
entry: "clearGetTemplateDataError",
invoke: {
src: "getActiveTemplateVersion",
onDone: [
{
actions: ["assignActiveTemplateVersion"],
target: "gettingTemplateVariables",
},
],
onError: {
actions: ["assignGetTemplateDataError"],
target: "error",
},
},
},
gettingTemplateVariables: {
entry: "clearGetTemplateDataError",
invoke: {
src: "getTemplateVariables",
onDone: [
{
actions: ["assignTemplateVariables"],
target: "fillingParams",
},
],
onError: {
actions: ["assignGetTemplateDataError"],
target: "error",
},
},
},
fillingParams: {
on: {
UPDATE_TEMPLATE_EVENT: {
actions: ["assignCreateTemplateVersionRequest", "clearJobError"],
target: "creatingTemplateVersion",
},
},
},
creatingTemplateVersion: {
entry: "clearUpdateTemplateError",
invoke: {
src: "createNewTemplateVersion",
onDone: {
actions: ["assignNewTemplateVersion"],
target: "waitingForJobToBeCompleted",
},
onError: {
actions: ["assignGetTemplateDataError"],
target: "fillingParams",
},
},
tags: ["submitting"],
},
waitingForJobToBeCompleted: {
invoke: {
src: "waitForJobToBeCompleted",
onDone: [
{
target: "fillingParams",
cond: "hasJobError",
actions: ["assignJobError"],
},
{
actions: ["assignNewTemplateVersion"],
target: "updatingTemplate",
},
],
onError: {
actions: ["assignUpdateTemplateError"],
target: "fillingParams",
},
},
tags: ["submitting"],
},
updatingTemplate: {
invoke: {
src: "updateTemplate",
onDone: {
target: "updated",
actions: ["onUpdateTemplate"],
},
onError: {
actions: ["assignUpdateTemplateError"],
target: "fillingParams",
},
},
tags: ["submitting"],
},
updated: {
entry: "onUpdateTemplate",
type: "final",
},
error: {},
},
},
{
services: {
getActiveTemplateVersion: ({ template }) => {
return getTemplateVersion(template.active_version_id);
},
getTemplateVariables: ({ template }) => {
return getTemplateVersionVariables(template.active_version_id);
},
createNewTemplateVersion: ({
organizationId,
createTemplateVersionRequest,
}) => {
if (!createTemplateVersionRequest) {
throw new Error("Missing request body");
}
return createTemplateVersion(
organizationId,
createTemplateVersionRequest,
);
},
waitForJobToBeCompleted: async ({ newTemplateVersion }) => {
if (!newTemplateVersion) {
throw new Error("Template version is undefined");
}
let status = newTemplateVersion.job.status;
while (["pending", "running"].includes(status)) {
newTemplateVersion = await getTemplateVersion(newTemplateVersion.id);
status = newTemplateVersion.job.status;
await delay(2_000);
}
return newTemplateVersion;
},
updateTemplate: ({ template, newTemplateVersion }) => {
if (!newTemplateVersion) {
throw new Error("New template version is undefined");
}
return updateActiveTemplateVersion(template.id, {
id: newTemplateVersion.id,
});
},
},
actions: {
assignActiveTemplateVersion: assign({
activeTemplateVersion: (_, event) => event.data,
}),
assignTemplateVariables: assign({
templateVariables: (_, event) => event.data,
}),
assignCreateTemplateVersionRequest: assign({
createTemplateVersionRequest: (_, event) => event.request,
}),
assignNewTemplateVersion: assign({
newTemplateVersion: (_, event) => event.data,
}),
assignGetTemplateDataError: assign({
getTemplateDataError: (_, event) => event.data,
}),
clearGetTemplateDataError: assign({
getTemplateDataError: (_) => undefined,
}),
assignUpdateTemplateError: assign({
updateTemplateError: (_, event) => event.data,
}),
clearUpdateTemplateError: assign({
updateTemplateError: (_) => undefined,
}),
assignJobError: assign({
jobError: (_, event) => event.data.job.error,
}),
clearJobError: assign({
jobError: (_) => undefined,
}),
},
guards: {
hasJobError: (_, { data }) => {
return Boolean(data.job.error);
},
},
},
);