feat: Add CardSelect component and integrate with Monitor creation form

This commit is contained in:
Nawaz Dhandala 2026-01-14 16:12:56 +00:00
parent 3631a48d83
commit d2c2f66b66
No known key found for this signature in database
GPG key ID: 96C5DCA24769DBCA
7 changed files with 165 additions and 3 deletions

View file

@ -1,4 +1,5 @@
import BadDataException from "../Exception/BadDataException";
import IconProp from "../Icon/IconProp";
enum MonitorType {
Manual = "Manual",
@ -30,6 +31,7 @@ export interface MonitorTypeProps {
monitorType: MonitorType;
description: string;
title: string;
icon: IconProp;
}
export class MonitorTypeHelper {
@ -53,24 +55,28 @@ export class MonitorTypeHelper {
title: "API",
description:
"This monitor type lets you monitor any API - GET, POST, PUT, DELETE or more.",
icon: IconProp.Code,
},
{
monitorType: MonitorType.Manual,
title: "Manual",
description:
"This monitor is a static monitor and will not actually monitor anything. It will however help you to integrate OneUptime with external monitoring tools and utilities.",
icon: IconProp.Wrench,
},
{
monitorType: MonitorType.Website,
title: "Website",
description:
"This monitor type lets you monitor landing pages like home page of your company / blog or more.",
icon: IconProp.Globe,
},
{
monitorType: MonitorType.Ping,
title: "Ping",
description:
"This monitor type does the basic ping test of an endpoint.",
icon: IconProp.Signal,
},
/*
* {
@ -78,6 +84,7 @@ export class MonitorTypeHelper {
* title: 'Kubernetes',
* description:
* 'This monitor types lets you monitor Kubernetes clusters.',
* icon: IconProp.Cube,
* },
*/
{
@ -85,70 +92,82 @@ export class MonitorTypeHelper {
title: "IP",
description:
"This monitor type lets you monitor any IPv4 or IPv6 addresses.",
icon: IconProp.AltGlobe,
},
{
monitorType: MonitorType.IncomingRequest,
title: "Incoming Request",
description:
"This monitor type lets you ping OneUptime from any external device or service with a custom payload.",
icon: IconProp.Webhook,
},
{
monitorType: MonitorType.IncomingEmail,
title: "Incoming Email",
description:
"This monitor type triggers alerts when emails are received at a unique email address with matching criteria.",
icon: IconProp.Email,
},
{
monitorType: MonitorType.Port,
title: "Port",
description: "This monitor type lets you monitor any TCP or UDP port.",
icon: IconProp.Terminal,
},
{
monitorType: MonitorType.Server,
title: "Server / VM",
description:
"This monitor type lets you monitor any server, VM, or any machine.",
icon: IconProp.Database,
},
{
monitorType: MonitorType.SSLCertificate,
title: "SSL Certificate",
description:
"This monitor type lets you monitor SSL certificates of any domain.",
icon: IconProp.ShieldCheck,
},
{
monitorType: MonitorType.SyntheticMonitor,
title: "Synthetic Monitor",
description:
"This monitor type lets you monitor your web application UI.",
icon: IconProp.Window,
},
{
monitorType: MonitorType.CustomJavaScriptCode,
title: "Custom JavaScript Code",
description:
"This monitor type lets you run custom JavaScript code on a schedule.",
icon: IconProp.Code,
},
{
monitorType: MonitorType.Logs,
title: "Logs",
description: "This monitor type lets you monitor logs from any source.",
icon: IconProp.Logs,
},
{
monitorType: MonitorType.Exceptions,
title: "Exceptions",
description:
"This monitor type lets you monitor exceptions and error groups from any source.",
icon: IconProp.Bug,
},
{
monitorType: MonitorType.Traces,
title: "Traces",
description:
"This monitor type lets you monitor traces from any source.",
icon: IconProp.Activity,
},
{
monitorType: MonitorType.Metrics,
title: "Metrics",
description:
"This monitor type lets you monitor metrics from any source.",
icon: IconProp.ChartBar,
},
];

View file

@ -0,0 +1,105 @@
import IconProp from "../../../Types/Icon/IconProp";
import Icon, { SizeProp } from "../Icon/Icon";
import React, { FunctionComponent, ReactElement } from "react";
export interface CardSelectOption {
value: string;
title: string;
description: string;
icon: IconProp;
}
export interface ComponentProps {
options: Array<CardSelectOption>;
value?: string | undefined;
onChange: (value: string) => void;
error?: string | undefined;
tabIndex?: number | undefined;
dataTestId?: string | undefined;
}
const CardSelect: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<div data-testid={props.dataTestId}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{props.options.map((option: CardSelectOption, index: number) => {
const isSelected: boolean = props.value === option.value;
return (
<div
key={index}
tabIndex={props.tabIndex ? props.tabIndex + index : index}
onClick={() => {
props.onChange(option.value);
}}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
props.onChange(option.value);
}
}}
className={`relative flex cursor-pointer rounded-lg border p-4 shadow-sm focus:outline-none transition-all duration-200 hover:border-indigo-500 hover:shadow-md ${
isSelected
? "border-indigo-600 ring-2 ring-indigo-600 bg-indigo-50"
: "border-gray-300 bg-white"
}`}
role="radio"
aria-checked={isSelected}
data-testid={`card-select-option-${option.value}`}
>
<div className="flex w-full items-start">
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
isSelected ? "bg-indigo-600" : "bg-gray-100"
}`}
>
<Icon
icon={option.icon}
size={SizeProp.Large}
className={`h-5 w-5 ${
isSelected ? "text-white" : "text-gray-600"
}`}
/>
</div>
<div className="ml-4 flex-1">
<span
className={`block text-sm font-semibold ${
isSelected ? "text-indigo-900" : "text-gray-900"
}`}
>
{option.title}
</span>
<span
className={`mt-1 block text-sm ${
isSelected ? "text-indigo-700" : "text-gray-500"
}`}
>
{option.description}
</span>
</div>
{isSelected && (
<div className="flex-shrink-0 ml-2">
<Icon
icon={IconProp.CheckCircle}
size={SizeProp.Large}
className="h-5 w-5 text-indigo-600"
/>
</div>
)}
</div>
</div>
);
})}
</div>
{props.error && (
<p className="mt-2 text-sm text-red-600" role="alert">
{props.error}
</p>
)}
</div>
);
};
export default CardSelect;

View file

@ -1,5 +1,6 @@
import Dictionary from "../../../../Types/Dictionary";
import { GetReactElementFunction } from "../../../Types/FunctionTypes";
import CardSelect from "../../CardSelect/CardSelect";
import CategoryCheckbox from "../../CategoryCheckbox/Index";
import CheckboxElement, {
CategoryCheckboxValue,
@ -366,6 +367,25 @@ const FormField: <T extends GenericObject>(
/>
)}
{props.field.fieldType === FormFieldSchemaType.CardSelect && (
<CardSelect
error={props.touched && props.error ? props.error : undefined}
tabIndex={index}
dataTestId={props.field.dataTestId}
onChange={(value: string) => {
onChange(value);
props.setFieldValue(props.fieldName, value);
}}
options={props.field.cardSelectOptions || []}
value={
props.currentValues &&
(props.currentValues as any)[props.fieldName]
? (props.currentValues as any)[props.fieldName]
: ""
}
/>
)}
{props.field.fieldType === FormFieldSchemaType.ObjectID && (
<IDGenerator
tabIndex={index}

View file

@ -3,6 +3,7 @@ import {
CategoryCheckboxOption,
CheckboxCategory,
} from "../../CategoryCheckbox/CategoryCheckboxTypes";
import { CardSelectOption } from "../../CardSelect/CardSelect";
import { DropdownOption } from "../../Dropdown/Dropdown";
import { RadioButton } from "../../RadioButtons/GroupRadioButtons";
import FormFieldSchemaType from "./FormFieldSchemaType";
@ -50,6 +51,7 @@ export default interface Field<TEntity> {
stepId?: string | undefined;
required?: boolean | ((item: FormValues<TEntity>) => boolean) | undefined;
dropdownOptions?: Array<DropdownOption> | undefined;
cardSelectOptions?: Array<CardSelectOption> | undefined;
fetchDropdownOptions?:
| ((item: FormValues<TEntity>) => Promise<Array<DropdownOption>>)
| undefined;

View file

@ -36,6 +36,7 @@ enum FormFieldSchemaType {
Checkbox = "Checkbox",
CategoryCheckbox = "CategoryCheckbox",
Dictionary = "Dictionary",
CardSelect = "CardSelect",
}
export default FormFieldSchemaType;

View file

@ -67,11 +67,11 @@ const MonitorCreate: FunctionComponent<
monitorType: true,
},
title: "Monitor Type",
description: "Select the type of monitor you want to create",
stepId: "monitor-info",
fieldType: FormFieldSchemaType.Dropdown,
fieldType: FormFieldSchemaType.CardSelect,
required: true,
placeholder: "Select Monitor Type",
dropdownOptions: MonitorTypeUtil.monitorTypesAsDropdownOptions(),
cardSelectOptions: MonitorTypeUtil.monitorTypesAsCardSelectOptions(),
},
{
field: {

View file

@ -2,6 +2,7 @@ import {
MonitorTypeHelper,
MonitorTypeProps,
} from "Common/Types/Monitor/MonitorType";
import { CardSelectOption } from "Common/UI/Components/CardSelect/CardSelect";
import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown";
export default class MonitorTypeUtil {
@ -16,4 +17,18 @@ export default class MonitorTypeUtil {
};
});
}
public static monitorTypesAsCardSelectOptions(): Array<CardSelectOption> {
const monitorTypes: Array<MonitorTypeProps> =
MonitorTypeHelper.getAllMonitorTypeProps();
return monitorTypes.map((props: MonitorTypeProps) => {
return {
value: props.monitorType,
title: props.title,
description: props.description,
icon: props.icon,
};
});
}
}