diff --git a/ui/app/alert-config/new.tsx b/ui/app/alert-config/new.tsx index aad7789029..ffaa41f91b 100644 --- a/ui/app/alert-config/new.tsx +++ b/ui/app/alert-config/new.tsx @@ -7,6 +7,17 @@ import { PulseLoader } from 'react-spinners'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { alertConfigReqSchema, alertConfigType } from './validation'; + +export interface AlertConfigProps { + id?: bigint; + serviceType: string; + authToken: string; + channelIdString: string; + slotLagGBAlertThreshold: number; + openConnectionsAlertThreshold: number; + forEdit?: boolean; +} + const notifyErr = (errMsg: string) => { toast.error(errMsg, { position: 'bottom-center', @@ -21,29 +32,30 @@ function ConfigLabel() { ); } -const NewAlertConfig = () => { - const [serviceType, setServiceType] = useState(); - const [authToken, setAuthToken] = useState(); - const [channelIdString, setChannelIdString] = useState(); - const [slotLagMBAlertThreshold, setSlotLagMBAlertThreshold] = - useState(); +const NewAlertConfig = (alertProps: AlertConfigProps) => { + const [serviceType, setServiceType] = useState('slack'); + const [authToken, setAuthToken] = useState(alertProps.authToken); + const [channelIdString, setChannelIdString] = useState( + alertProps.channelIdString + ); + const [slotLagGBAlertThreshold, setSlotLagGBAlertThreshold] = + useState(alertProps.slotLagGBAlertThreshold); const [openConnectionsAlertThreshold, setOpenConnectionsAlertThreshold] = - useState(); + useState(alertProps.openConnectionsAlertThreshold); const [loading, setLoading] = useState(false); const handleAdd = async () => { if (serviceType !== 'slack') { notifyErr('Service Type must be selected'); return; } - console.log(slotLagMBAlertThreshold); - console.log(openConnectionsAlertThreshold); + const alertConfigReq: alertConfigType = { serviceType: serviceType, serviceConfig: { auth_token: authToken ?? '', channel_ids: channelIdString?.split(',')!, - slot_lag_mb_alert_threshold: slotLagMBAlertThreshold || 0, - open_connections_alert_threshold: openConnectionsAlertThreshold || 0, + slot_lag_mb_alert_threshold: slotLagGBAlertThreshold * 1000 || 20000, + open_connections_alert_threshold: openConnectionsAlertThreshold || 5, }, }; const alertReqValidity = alertConfigReqSchema.safeParse(alertConfigReq); @@ -52,8 +64,11 @@ const NewAlertConfig = () => { return; } setLoading(true); + if (alertProps.forEdit) { + alertConfigReq.id = Number(alertProps.id); + } const createRes = await fetch('/api/alert-config', { - method: 'POST', + method: alertProps.forEdit ? 'PUT' : 'POST', body: JSON.stringify(alertConfigReq), }); const createStatus = await createRes.text(); @@ -86,6 +101,10 @@ const NewAlertConfig = () => { }, ]} placeholder='Select provider' + defaultValue={{ + value: 'slack', + label: 'Slack', + }} formatOptionLabel={ConfigLabel} onChange={(val, _) => val && setServiceType(val.value)} /> @@ -113,14 +132,14 @@ const NewAlertConfig = () => {
-

Slot Lag Alert Threshold (in MB)

+

Slot Lag Alert Threshold (in GB)

setSlotLagMBAlertThreshold(e.target.valueAsNumber)} + value={slotLagGBAlertThreshold} + onChange={(e) => setSlotLagGBAlertThreshold(e.target.valueAsNumber)} />
@@ -143,7 +162,13 @@ const NewAlertConfig = () => { onClick={handleAdd} variant='normalSolid' > - {loading ? : 'Create'} + {loading ? ( + + ) : alertProps.forEdit ? ( + 'Update' + ) : ( + 'Create' + )} diff --git a/ui/app/alert-config/page.tsx b/ui/app/alert-config/page.tsx index 4932255487..7d76872827 100644 --- a/ui/app/alert-config/page.tsx +++ b/ui/app/alert-config/page.tsx @@ -1,6 +1,6 @@ 'use client'; +import AlertDropdown from '@/components/AlertDropdown'; import ConfigJSONView from '@/components/ConfigJSONView'; -import { DropDialog } from '@/components/DropDialog'; import { Button } from '@/lib/Button'; import { Icon } from '@/lib/Icon'; import { Label } from '@/lib/Label'; @@ -11,8 +11,7 @@ import { PulseLoader } from 'react-spinners'; import useSWR from 'swr'; import { UAlertConfigResponse } from '../dto/AlertDTO'; import { fetcher } from '../utils/swr'; -import NewAlertConfig from './new'; - +import NewAlertConfig, { AlertConfigProps } from './new'; const ServiceIcon = (serviceType: string) => { switch (serviceType.toLowerCase()) { default: @@ -22,15 +21,40 @@ const ServiceIcon = (serviceType: string) => { const AlertConfigPage: React.FC = () => { const { data: alerts, - error, isLoading, }: { data: UAlertConfigResponse[]; error: any; isLoading: boolean; } = useSWR('/api/alert-config', fetcher); - const [newConfig, setNewConfig] = useState(false); + const blankAlert: AlertConfigProps = { + serviceType: '', + authToken: '', + channelIdString: '', + slotLagGBAlertThreshold: 20, + openConnectionsAlertThreshold: 5, + forEdit: false, + }; + const [inEditOrAddMode, setInEditOrAddMode] = useState(false); + const [editAlertConfig, setEditAlertConfig] = + useState(blankAlert); + const onEdit = (alertConfig: UAlertConfigResponse, id: bigint) => { + setInEditOrAddMode(true); + const configJSON = JSON.stringify(alertConfig.service_config); + const channelIds: string[] = JSON.parse(configJSON)?.channel_ids; + setEditAlertConfig({ + id, + serviceType: alertConfig.service_type, + authToken: JSON.parse(configJSON)?.auth_token, + channelIdString: channelIds.join(','), + slotLagGBAlertThreshold: + (JSON.parse(configJSON)?.slot_lag_mb_alert_threshold as number) / 1000, + openConnectionsAlertThreshold: + JSON.parse(configJSON)?.open_connections_alert_threshold, + forEdit: true, + }); + }; return (
@@ -54,16 +78,22 @@ const AlertConfigPage: React.FC = () => { > {alerts?.length ? ( - alerts.map((alert: UAlertConfigResponse, index) => ( + alerts.map((alertConfig: UAlertConfigResponse, index) => ( - {alert.id} - {ServiceIcon(alert.service_type)} + {alertConfig.id} + + + {ServiceIcon(alertConfig.service_type)} -
+
@@ -75,7 +105,11 @@ const AlertConfigPage: React.FC = () => { justifyContent: 'center', }} > - + onEdit(alertConfig, alertConfig.id)} + />
@@ -94,17 +128,21 @@ const AlertConfigPage: React.FC = () => { )} - {newConfig && } + {inEditOrAddMode && } ); }; diff --git a/ui/app/alert-config/validation.ts b/ui/app/alert-config/validation.ts index 0d2007b501..3256c51aa0 100644 --- a/ui/app/alert-config/validation.ts +++ b/ui/app/alert-config/validation.ts @@ -1,6 +1,7 @@ import z from 'zod'; export const alertConfigReqSchema = z.object({ + id: z.optional(z.number()), serviceType: z.enum(['slack'], { errorMap: (issue, ctx) => ({ message: 'Invalid service type' }), }), diff --git a/ui/app/api/alert-config/route.ts b/ui/app/api/alert-config/route.ts index 6966d2c146..7f66da191f 100644 --- a/ui/app/api/alert-config/route.ts +++ b/ui/app/api/alert-config/route.ts @@ -40,3 +40,21 @@ export async function DELETE(request: Request) { return new Response(deleteStatus); } + +export async function PUT(request: Request) { + const alertConfigReq: alertConfigType = await request.json(); + const editRes = await prisma.alerting_config.update({ + data: { + service_type: alertConfigReq.serviceType, + service_config: alertConfigReq.serviceConfig, + }, + where: { + id: alertConfigReq.id, + }, + }); + let editStatus: 'success' | 'error' = 'error'; + if (editRes.id) { + editStatus = 'success'; + } + return new Response(editStatus); +} diff --git a/ui/components/AlertDropdown.tsx b/ui/components/AlertDropdown.tsx new file mode 100644 index 0000000000..dd3ae482e3 --- /dev/null +++ b/ui/components/AlertDropdown.tsx @@ -0,0 +1,62 @@ +import { Button } from '@/lib/Button/Button'; +import { Icon } from '@/lib/Icon'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { useState } from 'react'; +import { DropDialog } from './DropDialog'; +const AlertDropdown = ({ + disable, + alertId, + onEdit, +}: { + disable: boolean; + alertId: bigint; + onEdit: () => void; +}) => { + const [open, setOpen] = useState(false); + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default AlertDropdown; diff --git a/ui/components/DropDialog.tsx b/ui/components/DropDialog.tsx index 3da1d8ffdd..63acb3ef2b 100644 --- a/ui/components/DropDialog.tsx +++ b/ui/components/DropDialog.tsx @@ -113,8 +113,14 @@ export const DropDialog = ({ noInteract={true} size='large' triggerButton={ - } > diff --git a/ui/package-lock.json b/ui/package-lock.json index 92d72704e5..c2a9f74420 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-form": "^0.0.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.0.7", @@ -3990,6 +3991,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", @@ -4109,6 +4139,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", diff --git a/ui/package.json b/ui/package.json index c9987de1e7..41575fcd7e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-form": "^0.0.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.0.7",