Skip to content

Commit

Permalink
feat: add waitFor to the UI
Browse files Browse the repository at this point in the history
Fixes #2344

fix: update favicon path to reference root

Update src/components/Forms/Formik/FormikDurationNanosecondsField.tsx

Co-authored-by: Moshe Immerman <[email protected]>

fix: fix calculations
  • Loading branch information
mainawycliffe authored and moshloop committed Oct 21, 2024
1 parent 4f3e474 commit 82e2c63
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/api/types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type NotificationRules = {
most_common_error?: string;
repeat_interval?: string;
error?: string;
wait_for?: number;
};

export type SilenceNotificationResponse = {
Expand Down
146 changes: 146 additions & 0 deletions src/components/Forms/Formik/FormikDurationNanosecondsField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import dayjs from "dayjs";
import { useField } from "formik";
import { useCallback, useMemo, useState } from "react";
import CreatableSelect from "react-select/creatable";

type FormikDurationNanosecondsFieldProps = {
name: string;
required?: boolean;
label?: string;
hint?: string;
hintPosition?: "top" | "bottom";
isClearable?: boolean;
};
export default function FormikDurationNanosecondsField({
name,
required = false,
label,
hint,
hintPosition = "bottom",
isClearable = true
}: FormikDurationNanosecondsFieldProps) {
const [isTouched, setIsTouched] = useState(false);

const [field, meta] = useField<string>({
name,
type: "text",
required,
validate: useCallback(
(value: string) => {
if (required && !value) {
return "This field is required";
}

// if value is less than 1 minute, show error
if (parseInt(value, 10) < 60 * 1e9) {
return "Duration must be greater than 1 minute";
}
},
[required]
)
});

const value = useMemo(() => {
// we want to take nanoseconds and convert them to 1h, 1m, 1s
if (!field.value) {
return undefined;
}

const duration = dayjs.duration(
parseInt(field.value, 10) / 1000000,
"milliseconds"
);
return `${duration.humanize()}`;
}, [field.value]);

const handleOnChange = (value?: string) => {
if (!value) {
field.onChange({
target: {
name: field.name,
value: ""
}
});
return;
}

// we want to take 1h, 1m, 1s and convert them to nanoseconds
let nanoseconds = 0;
if (value.includes("h")) {
nanoseconds = parseInt(value.replace("h", ""), 10) * 60 * 60 * 1e9;
} else if (value.includes("m")) {
// 1m = 60s
nanoseconds = parseInt(value.replace("m", ""), 10) * 60 * 1e9;
} else if (value.includes("s")) {
nanoseconds = parseInt(value.replace("s", ""), 10) * 1e9;
} else if (value.includes("d")) {
nanoseconds = parseInt(value.replace("d", ""), 10) * 24 * 60 * 60 * 1e9;
} else if (value.includes("w")) {
nanoseconds =
parseInt(value.replace("w", ""), 10) * 7 * 24 * 60 * 60 * 1e9;
}

field.onChange({
target: {
name: field.name,
value: nanoseconds
}
});
};

return (
<div className="flex flex-col">
{label && <label className="form-label mb-0">{label}</label>}
{hint && hintPosition === "top" && (
<p className="text-sm text-gray-500">{hint}</p>
)}
<CreatableSelect<{ label: string; value: string }>
className="h-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
onChange={(value) => {
handleOnChange(value?.value ?? undefined);
setIsTouched(true);
}}
value={[{ label: value!, value: value! }]}
options={[
"3m",
"5m",
"10m",
"15m",
"30m",
"1h",
"4h",
"8h",
"1d",
"3d",
"7d"
].map((value) => ({
label: value,
value
}))}
onBlur={(event) => {
field.onBlur(event);
setIsTouched(true);
}}
onFocus={(event) => {
field.onBlur(event);
setIsTouched(true);
}}
name={field.name}
isClearable={isClearable}
isMulti={false}
menuPortalTarget={document.body}
styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 })
}}
menuPosition={"fixed"}
menuShouldBlockScroll={true}
/>
{hint && hintPosition === "bottom" && (
<p className="text-sm text-gray-500">{hint}</p>
)}
{isTouched && meta.error ? (
<p className="w-full py-1 text-sm text-red-500">{meta.error}</p>
) : null}
</div>
);
}
6 changes: 6 additions & 0 deletions src/components/Notifications/Rules/NotificationsRulesForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NotificationRules } from "@flanksource-ui/api/types/notifications";
import FormikDurationNanosecondsField from "@flanksource-ui/components/Forms/Formik/FormikDurationNanosecondsField";
import { Form, Formik } from "formik";
import { Button } from "../../../ui/Buttons/Button";
import FormikAutocompleteDropdown from "../../Forms/Formik/FormikAutocompleteDropdown";
Expand Down Expand Up @@ -67,6 +68,11 @@ export default function NotificationsRulesForm({
name="repeat_interval"
label="Repeat Interval"
/>
<FormikDurationNanosecondsField
isClearable
name="wait_for"
label="Wait For"
/>
<FormikNotificationsTemplateField name="template" />
<FormikCodeEditor
fieldName="properties"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import MRTAvatarCell from "@flanksource-ui/ui/MRTDataTable/Cells/MRTAvataCell";
import { MRTDateCell } from "@flanksource-ui/ui/MRTDataTable/Cells/MRTDateCells";
import { MRTCellProps } from "@flanksource-ui/ui/MRTDataTable/MRTCellProps";
import { formatDuration } from "@flanksource-ui/utils/date";
import dayjs from "dayjs";
import { atom, useAtom } from "jotai";
import { MRT_ColumnDef } from "mantine-react-table";
import { useState } from "react";
Expand Down Expand Up @@ -285,6 +286,20 @@ export const notificationsRulesTableColumns: MRT_ColumnDef<NotificationRules>[]
return value;
}
},
{
header: "Wait For",
id: "wait_for",
accessorKey: "wait_for",
size: 130,
Cell: ({ row }) => {
const value = row.original.wait_for;
if (!value) {
return null;
}
// Convert nanoseconds to date
return dayjs.duration(value / 1000000, "milliseconds").humanize(false);
}
},
{
header: "Created At",
id: "created_at",
Expand Down

0 comments on commit 82e2c63

Please sign in to comment.