Skip to content

Commit 82e2c63

Browse files
mainawycliffemoshloop
authored andcommitted
feat: add waitFor to the UI
Fixes #2344 fix: update favicon path to reference root Update src/components/Forms/Formik/FormikDurationNanosecondsField.tsx Co-authored-by: Moshe Immerman <moshe@flanksource.com> fix: fix calculations
1 parent 4f3e474 commit 82e2c63

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

src/api/types/notifications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type NotificationRules = {
4040
most_common_error?: string;
4141
repeat_interval?: string;
4242
error?: string;
43+
wait_for?: number;
4344
};
4445

4546
export type SilenceNotificationResponse = {
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import dayjs from "dayjs";
2+
import { useField } from "formik";
3+
import { useCallback, useMemo, useState } from "react";
4+
import CreatableSelect from "react-select/creatable";
5+
6+
type FormikDurationNanosecondsFieldProps = {
7+
name: string;
8+
required?: boolean;
9+
label?: string;
10+
hint?: string;
11+
hintPosition?: "top" | "bottom";
12+
isClearable?: boolean;
13+
};
14+
export default function FormikDurationNanosecondsField({
15+
name,
16+
required = false,
17+
label,
18+
hint,
19+
hintPosition = "bottom",
20+
isClearable = true
21+
}: FormikDurationNanosecondsFieldProps) {
22+
const [isTouched, setIsTouched] = useState(false);
23+
24+
const [field, meta] = useField<string>({
25+
name,
26+
type: "text",
27+
required,
28+
validate: useCallback(
29+
(value: string) => {
30+
if (required && !value) {
31+
return "This field is required";
32+
}
33+
34+
// if value is less than 1 minute, show error
35+
if (parseInt(value, 10) < 60 * 1e9) {
36+
return "Duration must be greater than 1 minute";
37+
}
38+
},
39+
[required]
40+
)
41+
});
42+
43+
const value = useMemo(() => {
44+
// we want to take nanoseconds and convert them to 1h, 1m, 1s
45+
if (!field.value) {
46+
return undefined;
47+
}
48+
49+
const duration = dayjs.duration(
50+
parseInt(field.value, 10) / 1000000,
51+
"milliseconds"
52+
);
53+
return `${duration.humanize()}`;
54+
}, [field.value]);
55+
56+
const handleOnChange = (value?: string) => {
57+
if (!value) {
58+
field.onChange({
59+
target: {
60+
name: field.name,
61+
value: ""
62+
}
63+
});
64+
return;
65+
}
66+
67+
// we want to take 1h, 1m, 1s and convert them to nanoseconds
68+
let nanoseconds = 0;
69+
if (value.includes("h")) {
70+
nanoseconds = parseInt(value.replace("h", ""), 10) * 60 * 60 * 1e9;
71+
} else if (value.includes("m")) {
72+
// 1m = 60s
73+
nanoseconds = parseInt(value.replace("m", ""), 10) * 60 * 1e9;
74+
} else if (value.includes("s")) {
75+
nanoseconds = parseInt(value.replace("s", ""), 10) * 1e9;
76+
} else if (value.includes("d")) {
77+
nanoseconds = parseInt(value.replace("d", ""), 10) * 24 * 60 * 60 * 1e9;
78+
} else if (value.includes("w")) {
79+
nanoseconds =
80+
parseInt(value.replace("w", ""), 10) * 7 * 24 * 60 * 60 * 1e9;
81+
}
82+
83+
field.onChange({
84+
target: {
85+
name: field.name,
86+
value: nanoseconds
87+
}
88+
});
89+
};
90+
91+
return (
92+
<div className="flex flex-col">
93+
{label && <label className="form-label mb-0">{label}</label>}
94+
{hint && hintPosition === "top" && (
95+
<p className="text-sm text-gray-500">{hint}</p>
96+
)}
97+
<CreatableSelect<{ label: string; value: string }>
98+
className="h-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
99+
onChange={(value) => {
100+
handleOnChange(value?.value ?? undefined);
101+
setIsTouched(true);
102+
}}
103+
value={[{ label: value!, value: value! }]}
104+
options={[
105+
"3m",
106+
"5m",
107+
"10m",
108+
"15m",
109+
"30m",
110+
"1h",
111+
"4h",
112+
"8h",
113+
"1d",
114+
"3d",
115+
"7d"
116+
].map((value) => ({
117+
label: value,
118+
value
119+
}))}
120+
onBlur={(event) => {
121+
field.onBlur(event);
122+
setIsTouched(true);
123+
}}
124+
onFocus={(event) => {
125+
field.onBlur(event);
126+
setIsTouched(true);
127+
}}
128+
name={field.name}
129+
isClearable={isClearable}
130+
isMulti={false}
131+
menuPortalTarget={document.body}
132+
styles={{
133+
menuPortal: (base) => ({ ...base, zIndex: 9999 })
134+
}}
135+
menuPosition={"fixed"}
136+
menuShouldBlockScroll={true}
137+
/>
138+
{hint && hintPosition === "bottom" && (
139+
<p className="text-sm text-gray-500">{hint}</p>
140+
)}
141+
{isTouched && meta.error ? (
142+
<p className="w-full py-1 text-sm text-red-500">{meta.error}</p>
143+
) : null}
144+
</div>
145+
);
146+
}

src/components/Notifications/Rules/NotificationsRulesForm.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NotificationRules } from "@flanksource-ui/api/types/notifications";
2+
import FormikDurationNanosecondsField from "@flanksource-ui/components/Forms/Formik/FormikDurationNanosecondsField";
23
import { Form, Formik } from "formik";
34
import { Button } from "../../../ui/Buttons/Button";
45
import FormikAutocompleteDropdown from "../../Forms/Formik/FormikAutocompleteDropdown";
@@ -67,6 +68,11 @@ export default function NotificationsRulesForm({
6768
name="repeat_interval"
6869
label="Repeat Interval"
6970
/>
71+
<FormikDurationNanosecondsField
72+
isClearable
73+
name="wait_for"
74+
label="Wait For"
75+
/>
7076
<FormikNotificationsTemplateField name="template" />
7177
<FormikCodeEditor
7278
fieldName="properties"

src/components/Notifications/Rules/notificationsRulesTableColumns.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import MRTAvatarCell from "@flanksource-ui/ui/MRTDataTable/Cells/MRTAvataCell";
55
import { MRTDateCell } from "@flanksource-ui/ui/MRTDataTable/Cells/MRTDateCells";
66
import { MRTCellProps } from "@flanksource-ui/ui/MRTDataTable/MRTCellProps";
77
import { formatDuration } from "@flanksource-ui/utils/date";
8+
import dayjs from "dayjs";
89
import { atom, useAtom } from "jotai";
910
import { MRT_ColumnDef } from "mantine-react-table";
1011
import { useState } from "react";
@@ -285,6 +286,20 @@ export const notificationsRulesTableColumns: MRT_ColumnDef<NotificationRules>[]
285286
return value;
286287
}
287288
},
289+
{
290+
header: "Wait For",
291+
id: "wait_for",
292+
accessorKey: "wait_for",
293+
size: 130,
294+
Cell: ({ row }) => {
295+
const value = row.original.wait_for;
296+
if (!value) {
297+
return null;
298+
}
299+
// Convert nanoseconds to date
300+
return dayjs.duration(value / 1000000, "milliseconds").humanize(false);
301+
}
302+
},
288303
{
289304
header: "Created At",
290305
id: "created_at",

0 commit comments

Comments
 (0)