diff --git a/changes/27454-validation b/changes/27454-validation
new file mode 100644
index 000000000000..fd08c518e385
--- /dev/null
+++ b/changes/27454-validation
@@ -0,0 +1 @@
+- Accept any "http://" or "https://" prefixed Fleet web URL
diff --git a/frontend/components/forms/RegistrationForm/FleetDetails/FleetDetails.tests.jsx b/frontend/components/forms/RegistrationForm/FleetDetails/FleetDetails.tests.jsx
index 7ca5b9249dad..e447bfeaf31f 100644
--- a/frontend/components/forms/RegistrationForm/FleetDetails/FleetDetails.tests.jsx
+++ b/frontend/components/forms/RegistrationForm/FleetDetails/FleetDetails.tests.jsx
@@ -5,6 +5,8 @@ import { renderWithSetup } from "test/test-utils";
import FleetDetails from "components/forms/RegistrationForm/FleetDetails";
+import INVALID_SERVER_URL_MESSAGE from "utilities/error_messages";
+
describe("FleetDetails - form", () => {
const handleSubmitSpy = jest.fn();
it("renders", () => {
@@ -29,24 +31,30 @@ describe("FleetDetails - form", () => {
).toBeInTheDocument();
});
- it("validates the fleet web address field starts with https://", async () => {
+ it("validates the Fleet server URL field starts with 'https://' or 'http://'", async () => {
const { user } = renderWithSetup(
);
- await user.type(
- screen.getByRole("textbox", { name: "Fleet web address" }),
- "http://gnar.Fleet.co"
- );
- await user.click(screen.getByRole("button", { name: "Next" }));
+ const inputField = screen.getByRole("textbox", {
+ name: "Fleet web address",
+ });
+ const nextButton = screen.getByRole("button", { name: "Next" });
+
+ await user.type(inputField, "gnar.Fleet.co");
+ await user.click(nextButton);
expect(handleSubmitSpy).not.toHaveBeenCalled();
- expect(
- screen.getByText("Fleet web address must start with https://")
- ).toBeInTheDocument();
+ expect(screen.getByText(INVALID_SERVER_URL_MESSAGE)).toBeInTheDocument();
+
+ await user.type(inputField, "localhost:8080");
+ await user.click(nextButton);
+
+ expect(handleSubmitSpy).not.toHaveBeenCalled();
+ expect(screen.getByText(INVALID_SERVER_URL_MESSAGE)).toBeInTheDocument();
});
- it("submits the form when valid", async () => {
+ it("submits the form with valid https link", async () => {
const { user } = renderWithSetup(
);
@@ -61,4 +69,19 @@ describe("FleetDetails - form", () => {
server_url: "https://gnar.Fleet.co",
});
});
+ it("submits the form with valid http link", async () => {
+ const { user } = renderWithSetup(
+
+ );
+ // when
+ await user.type(
+ screen.getByRole("textbox", { name: "Fleet web address" }),
+ "http://localhost:8080"
+ );
+ await user.click(screen.getByRole("button", { name: "Next" }));
+ // then
+ expect(handleSubmitSpy).toHaveBeenCalledWith({
+ server_url: "http://localhost:8080",
+ });
+ });
});
diff --git a/frontend/components/forms/RegistrationForm/FleetDetails/helpers.js b/frontend/components/forms/RegistrationForm/FleetDetails/helpers.js
index f66547dfa737..d1c08acf1c7f 100644
--- a/frontend/components/forms/RegistrationForm/FleetDetails/helpers.js
+++ b/frontend/components/forms/RegistrationForm/FleetDetails/helpers.js
@@ -1,4 +1,8 @@
-import { size, startsWith } from "lodash";
+import { size } from "lodash";
+
+import validUrl from "components/forms/validators/valid_url";
+
+import INVALID_SERVER_URL_MESSAGE from "utilities/error_messages";
const validate = (formData) => {
const errors = {};
@@ -6,10 +10,14 @@ const validate = (formData) => {
if (!fleetWebAddress) {
errors.server_url = "Fleet web address must be completed";
- }
-
- if (fleetWebAddress && !startsWith(fleetWebAddress, "https://")) {
- errors.server_url = "Fleet web address must start with https://";
+ } else if (
+ !validUrl({
+ url: fleetWebAddress,
+ protocols: ["http", "https"],
+ allowLocalHost: true,
+ })
+ ) {
+ errors.server_url = INVALID_SERVER_URL_MESSAGE;
}
const valid = !size(errors);
diff --git a/frontend/components/forms/validators/valid_url/valid_url.ts b/frontend/components/forms/validators/valid_url/valid_url.ts
index 1707e991f1c8..4b94cd76ca40 100644
--- a/frontend/components/forms/validators/valid_url/valid_url.ts
+++ b/frontend/components/forms/validators/valid_url/valid_url.ts
@@ -6,8 +6,13 @@ interface IValidUrl {
url: string;
/** Validate protocols specified */
protocols?: ("http" | "https")[];
+ allowLocalHost?: boolean;
}
-export default ({ url, protocols }: IValidUrl): boolean => {
- return isURL(url, { protocols });
+export default ({ url, protocols, allowLocalHost = false }: IValidUrl) => {
+ return isURL(url, {
+ protocols,
+ require_protocol: !!protocols?.length,
+ require_tld: !allowLocalHost,
+ });
};
diff --git a/frontend/docs/patterns.md b/frontend/docs/patterns.md
index f417de7482c8..f17bad4f5ea3 100644
--- a/frontend/docs/patterns.md
+++ b/frontend/docs/patterns.md
@@ -208,9 +208,10 @@ When building a React-controlled form:
- Use the native HTML `form` element to wrap the form.
- Use a `Button` component with `type="submit"` for its submit button.
- Write a submit handler, e.g. `handleSubmit`, that accepts an `evt:
-React.FormEvent` argument and, critically, calls `evt.preventDefault()` in its
-body. This prevents the HTML `form`'s default submit behavior from interfering with our custom
+React.FormEvent` argument and, critically:
+ - calls `evt.preventDefault()` in its body. This prevents the HTML `form`'s default submit behavior from interfering with our custom
handler's logic.
+ - does nothing (e.g., returns `null`) if the form is in an invalid state, preventing submission by any means.
- Assign that handler to the `form`'s `onSubmit` property (*not* the submit button's `onClick`)
### Data validation
@@ -248,7 +249,9 @@ const onInputChange = ({ name, value }: IFormField) => {
// new errors are only set onBlur
const errsToSet: Record = {};
Object.keys(formErrors).forEach((k) => {
+ // @ts-ignore
if (newErrs[k]) {
+ // @ts-ignore
errsToSet[k] = newErrs[k];
}
});
@@ -270,7 +273,6 @@ const onInputBlur = () => {
```tsx
const onFormSubmit = (evt: React.MouseEvent) => {
evt.preventDefault();
-
// return null if there are errors
const errs = validateFormData(formData);
if (Object.keys(errs).length > 0) {
diff --git a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx
index f18a9b594826..c8f61836bef1 100644
--- a/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx
+++ b/frontend/pages/admin/OrgSettingsPage/cards/WebAddress/WebAddress.tsx
@@ -1,4 +1,5 @@
import React, { useState } from "react";
+import { size } from "lodash";
import Button from "components/buttons/Button";
// @ts-ignore
@@ -7,6 +8,8 @@ import validUrl from "components/forms/validators/valid_url";
import SectionHeader from "components/SectionHeader";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
+import INVALID_SERVER_URL_MESSAGE from "utilities/error_messages";
+
import { IAppConfigFormProps, IFormField } from "../constants";
interface IWebAddressFormData {
@@ -16,9 +19,25 @@ interface IWebAddressFormData {
interface IWebAddressFormErrors {
server_url?: string | null;
}
-
const baseClass = "app-config-form";
+const validateFormData = ({ serverURL }: IWebAddressFormData) => {
+ const errors: IWebAddressFormErrors = {};
+ if (!serverURL) {
+ errors.server_url = "Fleet server URL must be present";
+ } else if (
+ !validUrl({
+ url: serverURL,
+ protocols: ["http", "https"],
+ allowLocalHost: true,
+ })
+ ) {
+ errors.server_url = INVALID_SERVER_URL_MESSAGE;
+ }
+
+ return errors;
+};
+
const WebAddress = ({
appConfig,
handleSubmit,
@@ -35,23 +54,34 @@ const WebAddress = ({
const [formErrors, setFormErrors] = useState({});
const onInputChange = ({ name, value }: IFormField) => {
- setFormData({ ...formData, [name]: value });
- setFormErrors({});
+ const newFormData = { ...formData, [name]: value };
+ setFormData(newFormData);
+ const newErrs = validateFormData(newFormData);
+ // only set errors that are updates of existing errors
+ // new errors are only set onBlur
+ const errsToSet: Record = {};
+ Object.keys(formErrors).forEach((k) => {
+ // @ts-ignore
+ if (newErrs[k]) {
+ // @ts-ignore
+ errsToSet[k] = newErrs[k];
+ }
+ });
+ setFormErrors(errsToSet);
};
- const validateForm = () => {
- const errors: IWebAddressFormErrors = {};
- if (!serverURL) {
- errors.server_url = "Fleet server URL must be present";
- } else if (!validUrl({ url: serverURL, protocols: ["http", "https"] })) {
- errors.server_url = `${serverURL} is not a valid URL`;
- }
-
- setFormErrors(errors);
+ const onInputBlur = () => {
+ setFormErrors(validateFormData(formData));
};
- const onFormSubmit = (evt: React.MouseEvent) => {
+ const onFormSubmit = (evt: React.FormEvent) => {
evt.preventDefault();
+ // return null if there are errors
+ const errs = validateFormData(formData);
+ if (size(errs)) {
+ setFormErrors(errs);
+ return;
+ }
// Formatting of API not UI
const formDataToSubmit = {
@@ -79,7 +109,7 @@ const WebAddress = ({
name="serverURL"
value={serverURL}
parseTarget
- onBlur={validateForm}
+ onBlur={onInputBlur}
error={formErrors.server_url}
tooltip="The base URL of this instance for use in Fleet links."
disabled={gitOpsModeEnabled}
@@ -90,7 +120,7 @@ const WebAddress = ({