diff --git a/client/src/Components/inputs/AutoComplete.tsx b/client/src/Components/inputs/AutoComplete.tsx
index 4822a805f..9d3a8c7ef 100644
--- a/client/src/Components/inputs/AutoComplete.tsx
+++ b/client/src/Components/inputs/AutoComplete.tsx
@@ -15,6 +15,9 @@ type AutoCompleteInputProps = Omit<
fieldLabel?: string;
required?: boolean;
renderOptionContent?: (option: any) => React.ReactNode;
+ placeholder?: string;
+ error?: boolean;
+ helperText?: string;
};
export const AutoCompleteInput = ({
@@ -22,6 +25,9 @@ export const AutoCompleteInput = ({
required,
renderInput,
renderOptionContent,
+ placeholder,
+ error,
+ helperText,
...props
}: AutoCompleteInputProps) => {
const theme = useTheme();
@@ -30,7 +36,9 @@ export const AutoCompleteInput = ({
const defaultRenderInput = (params: any) => (
);
diff --git a/client/src/Components/inputs/forms/FormAutoCompleteField.tsx b/client/src/Components/inputs/forms/FormAutoCompleteField.tsx
new file mode 100644
index 000000000..104141b87
--- /dev/null
+++ b/client/src/Components/inputs/forms/FormAutoCompleteField.tsx
@@ -0,0 +1,49 @@
+import {
+ Controller,
+ useFormContext,
+ type FieldPath,
+ type FieldValues,
+} from "react-hook-form";
+import { Autocomplete } from "@/Components/inputs";
+
+export interface AutocompleteOption {
+ id: string;
+ name: string;
+}
+
+interface FormAutocompleteProps {
+ name: FieldPath;
+ options: O[];
+ fieldLabel?: string;
+ placeholder?: string;
+}
+
+export const FormAutocomplete = ({
+ name,
+ options,
+ fieldLabel,
+ placeholder,
+}: FormAutocompleteProps) => {
+ const { control } = useFormContext();
+ return (
+ (
+ option.name}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ value={options.find((o) => o.id === field.value) ?? null}
+ onChange={(_: unknown, newValue: O | null) => {
+ field.onChange(newValue?.id ?? "");
+ }}
+ fieldLabel={fieldLabel}
+ placeholder={placeholder}
+ error={!!fieldState.error}
+ helperText={fieldState.error?.message ?? ""}
+ />
+ )}
+ />
+ );
+};
diff --git a/client/src/Components/inputs/forms/FormMultiSelectField.tsx b/client/src/Components/inputs/forms/FormMultiSelectField.tsx
new file mode 100644
index 000000000..02aec4c95
--- /dev/null
+++ b/client/src/Components/inputs/forms/FormMultiSelectField.tsx
@@ -0,0 +1,93 @@
+import {
+ Controller,
+ useFormContext,
+ type FieldPath,
+ type FieldValues,
+} from "react-hook-form";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import IconButton from "@mui/material/IconButton";
+import Divider from "@mui/material/Divider";
+import { useTheme } from "@mui/material/styles";
+import { Trash2 } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { Autocomplete } from "@/Components/inputs";
+import { LAYOUT } from "@/Utils/Theme/constants";
+
+export interface MultiSelectOption {
+ id: string | number;
+ name: string;
+}
+
+interface FormMultiSelectFieldProps {
+ name: FieldPath;
+ options: O[];
+ fieldLabel?: string;
+ renderRow?: (option: O) => React.ReactNode;
+ renderOptionContent?: (option: O) => React.ReactNode;
+}
+
+export const FormMultiSelectField = ({
+ name,
+ options,
+ fieldLabel,
+ renderRow,
+ renderOptionContent,
+}: FormMultiSelectFieldProps) => {
+ const { control } = useFormContext();
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+ {
+ const selected = options.filter((o) => (field.value ?? []).includes(o.id));
+ return (
+
+ option.name}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ onChange={(_: unknown, newValue: O[]) => {
+ field.onChange(newValue.map((o) => o.id));
+ }}
+ fieldLabel={fieldLabel}
+ renderOptionContent={renderOptionContent}
+ error={!!fieldState.error}
+ helperText={fieldState.error?.message ?? ""}
+ />
+ {selected.map((option, index) => (
+
+ {renderRow ? (
+ renderRow(option)
+ ) : (
+ {option.name}
+ )}
+ {
+ field.onChange(
+ (field.value ?? []).filter((id: O["id"]) => id !== option.id)
+ );
+ }}
+ aria-label={t("common.buttons.remove")}
+ >
+
+
+ {index < selected.length - 1 && }
+
+ ))}
+
+ );
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/inputs/forms/FormRadioGroup.tsx b/client/src/Components/inputs/forms/FormRadioGroupField.tsx
similarity index 100%
rename from client/src/Components/inputs/forms/FormRadioGroup.tsx
rename to client/src/Components/inputs/forms/FormRadioGroupField.tsx
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
index e3ef10cab..6989cd7a6 100644
--- a/client/src/Pages/CreateMonitor/index.tsx
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -11,12 +11,10 @@ import { Trans, useTranslation } from "react-i18next";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import Link from "@mui/material/Link";
-import Divider from "@mui/material/Divider";
-import IconButton from "@mui/material/IconButton";
-import { Trash2 } from "lucide-react";
import { HeaderDeleteControls } from "@/Components/monitors";
import { GeoContinents } from "@/Types/GeoCheck";
+import Box from "@mui/material/Box";
import {
BasePage,
ColoredLabel,
@@ -24,10 +22,8 @@ import {
StepProgress,
} from "@/Components/design-elements";
import {
- RadioWithDescription,
Button,
Select,
- Autocomplete,
SwitchComponent as Switch,
SliderWithLabel,
Dialog,
@@ -56,7 +52,8 @@ import {
} from "@/Validation/monitor";
import { FormNumberField } from "@/Components/inputs/forms/FormNumberField";
import { FormTextField } from "@/Components/inputs/forms/FormTextField";
-import { FormRadioGroup } from "@/Components/inputs/forms/FormRadioGroup";
+import { FormRadioGroup } from "@/Components/inputs/forms/FormRadioGroupField";
+import { FormMultiSelectField } from "@/Components/inputs/forms/FormMultiSelectField";
interface GeneralSettingsConfig {
urlLabel: string;
@@ -358,6 +355,24 @@ const CreateMonitorPage = () => {
logger.debug("Monitor creation validation errors", errors);
};
+ const notificationOptions = useMemo(() => {
+ return (notifications ?? []).map((n) => ({
+ ...n,
+ name: n.notificationName,
+ }));
+ }, [notifications]);
+
+ const locationOptions = useMemo(
+ () =>
+ GeoContinents.map((continent) => ({
+ id: continent,
+ name: t(
+ `pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
+ ),
+ })),
+ [t]
+ );
+
return (
{
title={t("pages.createMonitor.form.notifications.title")}
subtitle={t("pages.createMonitor.form.notifications.description")}
rightContent={
- {
- // Map notifications to have 'name' property for Autocomplete
- const notificationOptions = (notifications ?? []).map((n) => ({
- ...n,
- name: n.notificationName,
- }));
- const selectedNotifications = notificationOptions.filter((n) =>
- (field.value ?? []).includes(n.id)
- );
- return (
-
- option.name}
- onChange={(_: unknown, newValue: typeof notificationOptions) => {
- field.onChange(newValue.map((n) => n.id));
- }}
- isOptionEqualToValue={(option, value) => option.id === value.id}
- />
- {selectedNotifications.length > 0 && (
-
- {selectedNotifications.map((notification, index) => (
-
-
- {notification.notificationName}
-
- {
- field.onChange(
- (field.value ?? []).filter(
- (id: string) => id !== notification.id
- )
- );
- }}
- aria-label="Remove notification"
- >
-
-
- {index < selectedNotifications.length - 1 && }
-
- ))}
-
- )}
-
- );
- }}
+ options={notificationOptions}
/>
}
/>
@@ -864,72 +822,23 @@ const CreateMonitorPage = () => {
title={t("pages.createMonitor.form.tags.title")}
subtitle={t("pages.createMonitor.form.tags.description")}
rightContent={
- {
- const tagOptions = tags ?? [];
- const selectedTags = tagOptions.filter((tag) =>
- (field.value ?? []).includes(tag.id)
- );
- return (
-
- option.name}
- onChange={(_: unknown, newValue: typeof tagOptions) => {
- field.onChange(newValue.map((tag) => tag.id));
- }}
- isOptionEqualToValue={(option, value) => option.id === value.id}
- renderOptionContent={(option) => (
-
- )}
- />
- {selectedTags.length > 0 && (
-
- {selectedTags.map((tag, index) => (
-
-
-
- {
- field.onChange(
- (field.value ?? []).filter(
- (id: string) => id !== tag.id
- )
- );
- }}
- aria-label="Remove tag"
- >
-
-
- {index < selectedTags.length - 1 && }
-
- ))}
-
- )}
-
- );
- }}
+ options={tags ?? []}
+ renderRow={(tag) => (
+
+
+
+ )}
+ renderOptionContent={(option) => (
+
+ )}
/>
}
/>
@@ -1016,77 +925,12 @@ const CreateMonitorPage = () => {
)}
/>
- {
- const selectedOptions = ALL_HTTP_STATUS_CODES.filter((option) =>
- field.value?.includes(option.id)
- );
- return (
-
- option.name}
- isOptionEqualToValue={(option, value) => option.id === value.id}
- onChange={(
- _: unknown,
- newValue: typeof ALL_HTTP_STATUS_CODES
- ) => {
- field.onChange(newValue?.map((v) => v.id) || []);
- }}
- fieldLabel={t(
- "pages.createMonitor.form.advanced.option.customUpCodes.label"
- )}
- />
-
- {t(
- "pages.createMonitor.form.advanced.option.customUpCodes.description"
- )}
-
- {selectedOptions.length > 0 && (
-
- {selectedOptions.map((option, index) => (
-
- {option.name}
- {
- field.onChange(
- field.value?.filter((id) => id !== option.id)
- );
- }}
- aria-label={t(
- "pages.createMonitor.form.advanced.option.customUpCodes.removeAriaLabel"
- )}
- >
-
-
- {index < selectedOptions.length - 1 && }
-
- ))}
-
- )}
-
- );
- }}
+ options={ALL_HTTP_STATUS_CODES}
/>
+
{watchedMethod !== "HEAD" && (
{
/>
{watchGeoCheckEnabled && (
- {
- // Map continents to have 'name' property for Autocomplete
- const locationOptions = GeoContinents.map((continent) => ({
- id: continent,
- name: t(
- `pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
- ),
- }));
- const selectedLocations = locationOptions.filter((loc) =>
- (field.value ?? []).includes(loc.id)
- );
- return (
-
- option.name}
- onChange={(
- _: unknown,
- newValue: typeof locationOptions
- ) => {
- field.onChange(newValue.map((loc) => loc.id));
- }}
- isOptionEqualToValue={(option, value) =>
- option.id === value.id
- }
- fieldLabel={t(
- "pages.createMonitor.form.geoChecks.option.locations.label"
- )}
- />
- {selectedLocations.length > 0 && (
-
- {selectedLocations.map((location, index) => (
-
- {location.name}
- {
- field.onChange(
- (field.value ?? []).filter(
- (id: string) => id !== location.id
- )
- );
- }}
- aria-label="Remove location"
- >
-
-
- {index < selectedLocations.length - 1 && }
-
- ))}
-
- )}
-
- );
- }}
+ options={locationOptions}
+ fieldLabel={t(
+ "pages.createMonitor.form.geoChecks.option.locations.label"
+ )}
/>