diff --git a/coral/src/app/components/Form.test.tsx b/coral/src/app/components/Form.test.tsx
index baae49e6d1..f13fdc3987 100644
--- a/coral/src/app/components/Form.test.tsx
+++ b/coral/src/app/components/Form.test.tsx
@@ -13,6 +13,7 @@ import {
RadioButtonGroup,
Textarea,
TextInput,
+ Checkbox,
} from "src/app/components/Form";
import {
renderForm,
@@ -47,7 +48,7 @@ describe("Form", () => {
};
const assertSubmitted = (
- data: Record
+ data: Record
) => {
expect(onSubmit).toHaveBeenCalledWith(data, expect.anything());
};
@@ -929,4 +930,39 @@ describe("Form", () => {
expect(errorMessage).toBeVisible();
});
});
+
+ describe("", () => {
+ const schema = z.object({
+ areYouForReal: z.boolean(),
+ });
+ type Schema = z.infer;
+
+ beforeEach(() => {
+ results = renderForm(
+ name={"areYouForReal"}>Are you sure?,
+ { schema, onSubmit, onError }
+ );
+ });
+
+ it("should render a Checkbox with correct label", () => {
+ const checkbox = screen.getByRole("checkbox", { name: "Are you sure?" });
+ expect(checkbox).toBeEnabled();
+ });
+
+ it("should default to Checkbox being unchecked when no default values are provided", async () => {
+ const checkbox = screen.getByRole("checkbox", { name: "Are you sure?" });
+ expect(checkbox).not.toBeChecked();
+ });
+
+ it("should sync value to form state when clicking Checkbox", async () => {
+ const checkbox = screen.getByRole("checkbox", { name: "Are you sure?" });
+ expect(checkbox).not.toBeChecked();
+
+ await user.click(checkbox);
+ expect(checkbox).toBeChecked();
+
+ await submit();
+ assertSubmitted({ areYouForReal: true });
+ });
+ });
});
diff --git a/coral/src/app/components/Form.tsx b/coral/src/app/components/Form.tsx
index 6501ef45f0..528ad2663c 100644
--- a/coral/src/app/components/Form.tsx
+++ b/coral/src/app/components/Form.tsx
@@ -13,6 +13,8 @@ import {
RadioButtonProps as BaseRadioButtonProps,
Textarea as BaseTextarea,
TextareaProps as BaseTextareaProps,
+ Checkbox as BaseCheckbox,
+ CheckboxProps as BaseCheckboxProps,
Option,
OptionType,
Button,
@@ -699,3 +701,24 @@ export const FileInput = (
const ctx = useFormContext();
return ;
};
+
+// ({
+ name,
+ formContext: form,
+ ...props
+}: BaseCheckboxProps & FormInputProps & FormRegisterProps) {
+ return ;
+}
+
+const CheckboxMemo = memo(_Checkbox) as typeof _Checkbox;
+
+// eslint-disable-next-line import/exports-last,import/group-exports
+export const Checkbox = (
+ props: FormInputProps & BaseCheckboxProps
+): React.ReactElement & BaseCheckboxProps> => {
+ const ctx = useFormContext();
+ return ;
+};
+
+TextInput.Skeleton = BaseInput.Skeleton;
diff --git a/coral/src/app/features/topics/details/schema/TopicDetailsSchema.test.tsx b/coral/src/app/features/topics/details/schema/TopicDetailsSchema.test.tsx
index 96e75f1858..0be8ee2aec 100644
--- a/coral/src/app/features/topics/details/schema/TopicDetailsSchema.test.tsx
+++ b/coral/src/app/features/topics/details/schema/TopicDetailsSchema.test.tsx
@@ -922,6 +922,179 @@ describe("TopicDetailsSchema", () => {
});
});
});
+
+ describe("enables topic owner to promote a schema even if it's not compatible", () => {
+ const originalConsoleError = console.error;
+
+ beforeEach(() => {
+ console.error = jest.fn();
+
+ mockedUseTopicDetails.mockReturnValue({
+ topicOverviewIsRefetching: false,
+ topicSchemasIsRefetching: false,
+ topicName: testTopicName,
+ environmentId: testEnvironmentId,
+ topicSchemas: testTopicSchemas,
+ setSchemaVersion: mockSetSchemaVersion,
+ topicOverview: { topicInfo: { topicOwner: true } },
+ });
+
+ customRender(
+
+
+ ,
+ {
+ memoryRouter: true,
+ queryClient: true,
+ }
+ );
+ });
+
+ afterEach(() => {
+ console.error = originalConsoleError;
+ cleanup();
+ jest.clearAllMocks();
+ });
+
+ it("gives user option to force register if request fails with certain error", async () => {
+ // The first response to the test should be the compatibility error
+ // second response will be the success
+ mockPromoteSchemaRequest
+ .mockRejectedValueOnce({
+ success: false,
+ message: "failure: Schema is not compatible",
+ })
+ .mockResolvedValue({
+ success: true,
+ message: "",
+ });
+
+ const checkBoxBefore = screen.queryByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ expect(checkBoxBefore).not.toBeInTheDocument();
+
+ const buttonPromote = screen.getByRole("button", { name: "Promote" });
+
+ await user.click(buttonPromote);
+
+ const modal = screen.getByRole("dialog");
+ const buttonRequest = within(modal).getByRole("button", {
+ name: "Request schema promotion",
+ });
+
+ await user.click(buttonRequest);
+
+ expect(mockPromoteSchemaRequest).toHaveBeenCalledWith({
+ forceRegister: false,
+ remarks: "",
+ schemaVersion: "3",
+ sourceEnvironment: "1",
+ targetEnvironment: "2",
+ topicName: "topic-name",
+ });
+
+ const checkboxToForceRegister = screen.getByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ expect(checkboxToForceRegister).toBeVisible();
+
+ expect(console.error).toHaveBeenCalledWith({
+ message: "failure: Schema is not compatible",
+ success: false,
+ });
+ });
+
+ it("enables user to force register the schema if needed", async () => {
+ // The first response to the test should be the compatibility error
+ // second response will be the success
+ mockPromoteSchemaRequest
+ .mockRejectedValueOnce({
+ success: false,
+ message: "failure: Schema is not compatible",
+ })
+ .mockResolvedValue({
+ success: true,
+ message: "",
+ });
+
+ const buttonPromote = screen.getByRole("button", { name: "Promote" });
+
+ await user.click(buttonPromote);
+
+ const modal = screen.getByRole("dialog");
+ const buttonRequest = within(modal).getByRole("button", {
+ name: "Request schema promotion",
+ });
+
+ await user.click(buttonRequest);
+
+ const checkboxToForceRegister = screen.getByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ await user.click(checkboxToForceRegister);
+ await user.click(buttonRequest);
+
+ expect(mockPromoteSchemaRequest).toHaveBeenNthCalledWith(2, {
+ forceRegister: true,
+ remarks: "",
+ schemaVersion: "3",
+ sourceEnvironment: "1",
+ targetEnvironment: "2",
+ topicName: "topic-name",
+ });
+
+ expect(console.error).toHaveBeenCalledWith({
+ message: "failure: Schema is not compatible",
+ success: false,
+ });
+ });
+
+ it("shows an error if promotion with force register did fail", async () => {
+ // The first response to the test should be the compatibility error
+ // second response will be the success
+ mockPromoteSchemaRequest
+ .mockRejectedValueOnce({
+ success: false,
+ message: "failure: Schema is not compatible",
+ })
+ .mockRejectedValue({
+ success: false,
+ message: "Oh no",
+ });
+
+ const buttonPromote = screen.getByRole("button", { name: "Promote" });
+
+ await user.click(buttonPromote);
+
+ const modal = screen.getByRole("dialog");
+ const buttonRequest = within(modal).getByRole("button", {
+ name: "Request schema promotion",
+ });
+
+ await user.click(buttonRequest);
+
+ const checkboxToForceRegister = screen.getByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ await user.click(checkboxToForceRegister);
+ await user.click(buttonRequest);
+
+ const alert = screen.getByRole("alert");
+ const errorMessage = within(alert).getByText("Oh no");
+
+ expect(alert).toBeVisible();
+ expect(errorMessage).toBeVisible();
+ expect(console.error).toHaveBeenNthCalledWith(2, {
+ success: false,
+ message: "Oh no",
+ });
+ });
+ });
});
describe("renders right view for user that is not topic owner", () => {
diff --git a/coral/src/app/features/topics/details/schema/TopicDetailsSchema.tsx b/coral/src/app/features/topics/details/schema/TopicDetailsSchema.tsx
index 2ded0c5d62..f9d718a10e 100644
--- a/coral/src/app/features/topics/details/schema/TopicDetailsSchema.tsx
+++ b/coral/src/app/features/topics/details/schema/TopicDetailsSchema.tsx
@@ -51,6 +51,7 @@ function TopicDetailsSchema() {
const [showSchemaPromotionModal, setShowSchemaPromotionModal] =
useState(false);
const [errorMessage, setErrorMessage] = useState("");
+ const [isValidationError, setIsValidationError] = useState(false);
const toast = useToast();
@@ -88,10 +89,17 @@ function TopicDetailsSchema() {
},
{
onError: (error: HTTPError) => {
- setErrorMessage(parseErrorMsg(error));
- setShowSchemaPromotionModal(false);
+ const message = parseErrorMsg(error);
+ if (message.includes("Schema is not compatible")) {
+ setIsValidationError(true);
+ } else {
+ setErrorMessage(message);
+ setShowSchemaPromotionModal(false);
+ setIsValidationError(false);
+ }
},
onSuccess: () => {
+ setIsValidationError(false);
setErrorMessage("");
queryClient.refetchQueries(["schema-overview"]).then(() => {
setShowSchemaPromotionModal(false);
@@ -132,10 +140,7 @@ function TopicDetailsSchema() {
version={schemaDetailsPerEnv.version}
// We only allow users to use the forceRegister option when the promotion request failed
// And the failure is because of a schema compatibility issue
- showForceRegister={
- errorMessage.length > 0 &&
- errorMessage.includes("Schema is not compatible")
- }
+ showForceRegister={isValidationError}
/>
)}
diff --git a/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.test.tsx b/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.test.tsx
index 144fe0bdab..5f320eb324 100644
--- a/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.test.tsx
+++ b/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.test.tsx
@@ -35,6 +35,14 @@ describe("SchemaPromotionModal", () => {
expect(dialog).toBeVisible();
});
+ it("shows no warning or option to force register", () => {
+ const warning = screen.queryByRole("alert");
+ const checkbox = screen.queryByRole("checkbox");
+
+ expect(warning).not.toBeInTheDocument();
+ expect(checkbox).not.toBeInTheDocument();
+ });
+
it("shows more information to delete the topic", () => {
const dialog = screen.getByRole("dialog");
const headline = within(dialog).getByRole("heading", {
@@ -50,7 +58,7 @@ describe("SchemaPromotionModal", () => {
it("does not show a switch to force register", () => {
const forceRegisterSwitch = screen.queryByRole("checkbox", {
- name: "Force register Overrides some validation that the schema registry would normally do.",
+ name: "Force register Overrides standard validation processes of the schema registry.",
});
expect(forceRegisterSwitch).not.toBeInTheDocument();
});
@@ -102,13 +110,6 @@ describe("SchemaPromotionModal", () => {
jest.clearAllMocks();
});
- it("disables Force register switch", () => {
- const forceRegisterSwitch = screen.getByRole("checkbox", {
- name: "Force register Overrides some validation that the schema registry would normally do.",
- });
- expect(forceRegisterSwitch).toBeDisabled();
- });
-
it("disables textarea where user can add a comment why they promote the schema", () => {
const dialog = screen.getByRole("dialog");
const textarea = within(dialog).getByRole("textbox", {
@@ -191,6 +192,15 @@ describe("SchemaPromotionModal", () => {
jest.clearAllMocks();
});
+ it("shows a warning about force register", async () => {
+ const warning = screen.getByRole("alert");
+
+ expect(warning).toBeVisible();
+ expect(warning).toHaveTextContent(
+ "Uploaded schema appears invalid. Are you sure you want to force register it?"
+ );
+ });
+
it("triggers a given submit function with correct payload when user does not switch Force register or adds a reason", async () => {
const dialog = screen.getByRole("dialog");
@@ -211,7 +221,7 @@ describe("SchemaPromotionModal", () => {
const dialog = screen.getByRole("dialog");
const forceRegisterSwitch = screen.getByRole("checkbox", {
- name: "Force register Overrides some validation that the schema registry would normally do.",
+ name: "Force register Overrides standard validation processes of the schema registry.",
});
const confirmationButton = within(dialog).getByRole("button", {
@@ -232,7 +242,7 @@ describe("SchemaPromotionModal", () => {
const dialog = screen.getByRole("dialog");
const forceRegisterSwitch = screen.getByRole("checkbox", {
- name: "Force register Overrides some validation that the schema registry would normally do.",
+ name: "Force register Overrides standard validation processes of the schema registry.",
});
const textarea = within(dialog).getByRole("textbox", {
diff --git a/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.tsx b/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.tsx
index 1194474d5f..cc689b1977 100644
--- a/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.tsx
+++ b/coral/src/app/features/topics/details/schema/components/SchemaPromotionModal.tsx
@@ -1,4 +1,4 @@
-import { Box, Checkbox, Textarea } from "@aivenio/aquarium";
+import { Alert, Box, Checkbox, Textarea } from "@aivenio/aquarium";
import { useState } from "react";
import { Modal } from "src/app/components/Modal";
@@ -52,16 +52,12 @@ const SchemaPromotionModal = ({
{`Promote the Version ${version} of the schema to ${targetEnvironment}?`}
{showForceRegister && (
- setForceRegister(e.target.checked)}
- >
- Force register
-
+
+
+ Uploaded schema appears invalid. Are you sure you want to force
+ register it?
+
+
)}
+ {showForceRegister && (
+ setForceRegister(e.target.checked)}
+ >
+ Force register
+
+ )}
);
diff --git a/coral/src/app/features/topics/schema-request/TopicSchemaRequest.test.tsx b/coral/src/app/features/topics/schema-request/TopicSchemaRequest.test.tsx
index 1ea2fd5009..691be0cdcc 100644
--- a/coral/src/app/features/topics/schema-request/TopicSchemaRequest.test.tsx
+++ b/coral/src/app/features/topics/schema-request/TopicSchemaRequest.test.tsx
@@ -929,4 +929,141 @@ describe("TopicSchemaRequest", () => {
);
});
});
+
+ describe("enables user to send a schema request even if it's not compatible", () => {
+ const originalConsoleError = console.error;
+
+ beforeEach(async () => {
+ console.error = jest.fn();
+ mockGetAllEnvironmentsForTopicAndAcl.mockResolvedValue(
+ mockedGetAllEnvironmentsForTopicAndAclResponse
+ );
+ mockGetTopicNames.mockResolvedValue([testTopicName]);
+
+ // The first response to the test should be the compatibility error
+ // second response will be the success
+ mockCreateSchemaRequest
+ .mockRejectedValueOnce({
+ success: false,
+ message: "failure: Schema is not compatible",
+ })
+ .mockResolvedValue({
+ success: true,
+ message: "",
+ });
+
+ customRender(
+ ,
+ {
+ queryClient: true,
+ memoryRouter: true,
+ }
+ );
+ await waitForElementToBeRemoved(
+ screen.getByTestId("environments-select-loading")
+ );
+ });
+
+ afterEach(() => {
+ console.error = originalConsoleError;
+ mockedUseToast.mockReset();
+ cleanup();
+ });
+
+ it("gives user the option to force register", async () => {
+ const form = getForm();
+
+ const checkBoxBefore = within(form).queryByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ const warningBefore = screen.queryByText("alertdialog");
+ expect(checkBoxBefore).not.toBeInTheDocument();
+ expect(warningBefore).not.toBeInTheDocument();
+
+ const select = within(form).getByRole("combobox", {
+ name: /Environment/i,
+ });
+ const option = within(select).getByRole("option", {
+ name: mockedEnvironments[0].name,
+ });
+ const fileInput =
+ within(form).getByLabelText(/Upload AVRO Schema/i);
+
+ const button = within(form).getByRole("button", {
+ name: "Submit request",
+ });
+
+ await userEvent.selectOptions(select, option);
+ await userEvent.tab();
+ await userEvent.upload(fileInput, testFile);
+ await userEvent.click(button);
+
+ const warningForceRegister = screen.getByRole("alert");
+ const checkboxForceRegister = within(form).getByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+ expect(warningForceRegister).toBeVisible();
+ expect(warningForceRegister).toHaveTextContent(
+ "Uploaded schema appears invalid. Are you sure you want to" +
+ " force register it?"
+ );
+
+ expect(checkboxForceRegister).toBeVisible();
+ expect(console.error).toHaveBeenCalledWith({
+ message: "failure: Schema is not compatible",
+ success: false,
+ });
+ });
+
+ it("shows a notification informing user that force register for schema request was successful and redirects them", async () => {
+ const form = getForm();
+
+ const select = within(form).getByRole("combobox", {
+ name: /Environment/i,
+ });
+ const option = within(select).getByRole("option", {
+ name: mockedEnvironments[0].name,
+ });
+ const fileInput =
+ within(form).getByLabelText(/Upload AVRO Schema/i);
+ const submitButton = within(form).getByRole("button", {
+ name: "Submit request",
+ });
+
+ await userEvent.selectOptions(select, option);
+ await userEvent.tab();
+ await userEvent.upload(fileInput, testFile);
+ await userEvent.click(submitButton);
+
+ const checkboxForceRegister = within(form).getByRole("checkbox", {
+ name: "Force register Overrides standard validation processes of the schema registry.",
+ });
+
+ await userEvent.click(checkboxForceRegister);
+ await userEvent.click(submitButton);
+
+ expect(mockCreateSchemaRequest).toHaveBeenNthCalledWith(2, {
+ forceRegister: true,
+ environment: "1",
+ remarks: "",
+ schemafull: "{}",
+ topicname: "my-awesome-topic",
+ });
+
+ await waitFor(() =>
+ expect(mockedUseToast).toHaveBeenCalledWith({
+ message: "Schema request successfully created",
+ position: "bottom-left",
+ variant: "default",
+ })
+ );
+ expect(mockedUsedNavigate).toHaveBeenCalledWith(
+ "/requests/schemas?status=CREATED"
+ );
+ });
+ });
});
diff --git a/coral/src/app/features/topics/schema-request/TopicSchemaRequest.tsx b/coral/src/app/features/topics/schema-request/TopicSchemaRequest.tsx
index 01e0c84c84..8fe886b0c3 100644
--- a/coral/src/app/features/topics/schema-request/TopicSchemaRequest.tsx
+++ b/coral/src/app/features/topics/schema-request/TopicSchemaRequest.tsx
@@ -9,6 +9,7 @@ import {
SubmitButton,
Textarea,
useForm,
+ Checkbox,
} from "src/app/components/Form";
import { TopicSchema } from "src/app/features/topics/schema-request/components/TopicSchema";
import {
@@ -22,6 +23,7 @@ import {
import { requestSchemaCreation } from "src/domain/schema-request";
import { TopicNames, getTopicNames } from "src/domain/topic";
import { parseErrorMsg } from "src/services/mutation-utils";
+import { KlawApiError } from "src/services/api";
type TopicSchemaRequestProps = {
topicName?: string;
@@ -42,6 +44,7 @@ function TopicSchemaRequest(props: TopicSchemaRequestProps) {
const presetEnvironment = searchParams.get("env");
const [cancelDialogVisible, setCancelDialogVisible] = useState(false);
+ const [isValidationError, setIsValidationError] = useState(false);
const navigate = useNavigate();
const toast = useToast();
@@ -115,6 +118,7 @@ function TopicSchemaRequest(props: TopicSchemaRequestProps) {
const schemaRequestMutation = useMutation(requestSchemaCreation, {
onSuccess: () => {
+ setIsValidationError(false);
navigate("/requests/schemas?status=CREATED");
toast({
message: "Schema request successfully created",
@@ -122,6 +126,20 @@ function TopicSchemaRequest(props: TopicSchemaRequestProps) {
variant: "default",
});
},
+ onError: (error: KlawApiError | Error) => {
+ const validationErrorMessages = [
+ "schema is not compatible",
+ "unable to validate schema compatibility",
+ ];
+
+ const matchedError = validationErrorMessages.some((errorMessage) => {
+ return error?.message
+ .toLowerCase()
+ .includes(errorMessage.toLowerCase());
+ });
+
+ setIsValidationError(matchedError);
+ },
});
function onSubmitForm(userInput: TopicRequestFormSchema) {
@@ -147,13 +165,21 @@ function TopicSchemaRequest(props: TopicSchemaRequestProps) {
)}
- {schemaRequestMutation.isError && (
+ {schemaRequestMutation.isError && !isValidationError && (
{parseErrorMsg(schemaRequestMutation.error)}
)}
+ {isValidationError && (
+
+
+ Uploaded schema appears invalid. Are you sure you want to force
+ register it?
+
+
+ )}