diff --git a/app/(api)/api/page.tsx b/app/(api)/api/page.tsx index 1f231c48..28005a58 100644 --- a/app/(api)/api/page.tsx +++ b/app/(api)/api/page.tsx @@ -1,7 +1,6 @@ "use client"; - -import React, { useState, useEffect } from "react"; import dynamic from "next/dynamic"; +import { useState } from "react"; const DynamicReactSwagger = dynamic(() => import("./react-swagger"), { ssr: false, @@ -10,23 +9,33 @@ const DynamicReactSwagger = dynamic(() => import("./react-swagger"), { export default function IndexPage() { const [spec, setSpec] = useState(null); + const [error, setError] = useState(false); - useEffect(() => { - async function fetchSpecs() { + (async function fetchSpecs() { + if (spec || error) return; + try { const response = await fetch("swagger/doc.json"); - if (response.ok) { + if (!response.ok) throw new Error("Failed to fetch API docs"); const fetchedSpec = await response.json(); setSpec(fetchedSpec); - } else { - console.error("Failed to fetch API docs"); - } + } catch (err) { + console.error("Error fetching Swagger specs:", err); + setError(true); } - - fetchSpecs(); - }, []); - - if (!spec) return
Loading...
; + })(); + + if (error) { + return
Error loading API documentation. Please try again later.
; + } + + if (!spec) { + return ( +
+ Loading API documentation... +
+ ); + } return (
diff --git a/components/NetworkSliceModal.tsx b/components/NetworkSliceModal.tsx index bf6df4ee..bf42e86c 100644 --- a/components/NetworkSliceModal.tsx +++ b/components/NetworkSliceModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Input, Notification, @@ -36,8 +36,8 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo const auth = useAuth() const queryClient = useQueryClient(); const [apiError, setApiError] = useState(null); - const [upfApiError, setUpfApiError] = useState(null); const [gnbApiError, setGnbApiError] = useState(null); + const [upfApiError, setUpfApiError] = useState(null); const NetworkSliceSchema = Yup.object().shape({ name: Yup.string() @@ -58,10 +58,16 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo .required("MNC is required."), upf: Yup.object() .shape({ hostname: Yup.string().required("Please select a UPF.") }) + .shape({ port: Yup.string().required("Please select a UPF.") }) .required("Selecting a UPF is required."), gnbList: Yup.array() - .min(1) - .required("Selecting at least 1 gNodeB is required."), + .of( + Yup.object().shape({ + name: Yup.string().required("gNodeB name is required."), + tac: Yup.string().required("gNodeB TAC is required."), + }) + ) + .min(1, "Selecting at least 1 gNodeB is required."), }); const modalTitle = () => { @@ -94,6 +100,14 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo validationSchema: NetworkSliceSchema, onSubmit: async (values) => { try { + if (upfItems.length === 0) { + setUpfApiError("No available UPF. Please add at least one UPF."); + return; + } + if (gnbItems.length === 0) { + setGnbApiError("No available GNB. Please add at least one GNB."); + return; + } if (networkSlice) { await editNetworkSlice({ name: values.name, @@ -128,55 +142,29 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo }, }); - const { data: upfList = [], isLoading: isUpfLoading, isError: isUpfError } = useQuery({ + const upfQuery = useQuery({ queryKey: [queryKeys.upfList, auth.user?.authToken], - queryFn: () => getUpfList(auth.user ? auth.user.authToken : ""), - enabled: auth.user ? true : false, + queryFn: () => getUpfList(auth.user!.authToken), + enabled: auth.user ? true : false }); - useEffect(() => { - const checkUpfList = async () => { - if (isUpfError) { - setUpfApiError("Failed to retrieve the list of UPFs from the server.") - } else if (!isUpfLoading && upfList.length === 0) { - setUpfApiError("No available UPF. Please add at least one UPF."); - } - }; - checkUpfList(); - }, [isUpfLoading, isUpfError, upfList]); - - const { data: gnbList = [], isLoading: isGnbLoading, isError: isGnbError } = useQuery({ + const gnbQuery = useQuery({ queryKey: [queryKeys.gnbList, auth.user?.authToken], - queryFn: () => getGnbList(auth.user ? auth.user.authToken : ""), - enabled: auth.user ? true : false, + queryFn: () => getGnbList(auth.user!.authToken), + enabled: auth.user ? true : false }); - useEffect(() => { - const checkGnbList = async () => { - if (isGnbError) { - setGnbApiError("Failed to retrieve the list of GNBs from the server.") - } else if (!isGnbLoading && gnbList.length === 0) { - setGnbApiError("No available GNB. Please add at least one GNB."); - } - }; - checkGnbList(); - }, [isGnbLoading, isGnbError, gnbList]); + const upfItems = (upfQuery.data as UpfItem[]) || []; + const gnbItems = (gnbQuery.data as GnbItem[]) || []; const handleUpfChange = (e: React.ChangeEvent) => { - const upf = upfList.find( - (item) => e.target.value === `${item.hostname}:${item.port}`, - ); + const upf = upfItems.find((item: UpfItem) => e.target.value === `${item.hostname}:${item.port}`); void formik.setFieldValue("upf", upf); }; const handleGnbChange = (e: React.ChangeEvent) => { - const selectedOptions = Array.from(e.target.selectedOptions); - const items = gnbList.filter((item) => - selectedOptions.some( - (option) => option.value === `${item.name}:${item.tac}`, - ), - ); - void formik.setFieldValue("gnbList", items); + const gnb = gnbItems.find((item: GnbItem) => e.target.value === `${item.name}:${item.tac}`); + void formik.setFieldValue("gnbList", [...formik.values.gnbList, gnb]); }; const getGnbListValueAsString = () => { @@ -189,6 +177,42 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo return formik.values.upf.hostname ? `${formik.values.upf.hostname}:${formik.values.upf.port}` : ""; }; + const ErrorNotification = ({ + error, + }: { + error: string | null; + }) => { + return error ? ( + + {error} + + ) : null; + }; + + if (upfQuery.isLoading || gnbQuery.isLoading) { + return ( + + Fetching data... + + ); + } + + if (upfQuery.isError) { + return ( + + Failed to retrieve the list of UPFs from the server. + + ); + } + + if (gnbQuery.isError) { + return ( + + Failed to retrieve the list of gNodeBs from the server. + + ); + } + return ( } > - {apiError && ( - - {apiError} - - )} - {upfApiError && ( - - {upfApiError} - - )} - {gnbApiError && ( - - {gnbApiError} - - )} + {apiError && } + {upfApiError && } + {gnbApiError && }
({ + ...upfItems.map((upf) => ({ label: `${upf.hostname}:${upf.port}`, value: `${upf.hostname}:${upf.port}`, })), @@ -286,7 +298,7 @@ const NetworkSliceModal = ({ networkSlice, toggleModal, onSave }: NetworkSliceMo disabled: true, label: "Select...", }, - ...gnbList.map((gnb) => ({ + ...gnbItems.map((gnb) => ({ label: `${gnb.name} (tac:${gnb.tac})`, value: `${gnb.name}:${gnb.tac}`, })), diff --git a/components/SubscriberModal.tsx b/components/SubscriberModal.tsx index 8916295d..472fa168 100644 --- a/components/SubscriberModal.tsx +++ b/components/SubscriberModal.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useState } from "react"; import { ActionButton, Form, @@ -36,13 +36,16 @@ type Props = { const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubmit }: Props) => { const queryClient = useQueryClient(); - const auth = useAuth() + const auth = useAuth(); const [apiError, setApiError] = useState(null); const rawIMSI = subscriber?.ueId.split("-")[1]; + const token = auth.user?.authToken || ""; + + const oldDeviceGroup = + deviceGroups.find((deviceGroup) => + deviceGroup["imsis"]?.includes(rawIMSI) + ) || {}; - const oldDeviceGroup = deviceGroups.find( - (deviceGroup) => deviceGroup["imsis"]?.includes(rawIMSI) - ); const oldDeviceGroupName: string = oldDeviceGroup ? oldDeviceGroup["group-name"] : ""; const oldNetworkSlice = slices.find( @@ -52,33 +55,25 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm const SubscriberSchema = Yup.object().shape({ imsi: Yup.string() - .min(14) - .max(15) - .matches(/^[0-9]+$/, { message: "Only numbers are allowed." }) - .required("IMSI must be 14 or 15 digits"), + .min(14, "IMSI must be at least 14 digits") + .max(15, "IMSI must be at most 15 digits") + .matches(/^[0-9]+$/, "Only numbers are allowed.") + .required("IMSI is required"), opc: Yup.string() - .length(32) - .matches(/^[A-Za-z0-9]+$/, { - message: "Only alphanumeric characters are allowed.", - }) - .required("OPC must be a 32 character hexadecimal"), + .length(32, "OPC must be 32 hexadecimal characters") + .matches(/^[A-Za-z0-9]+$/, "Use valid hexadecimal characters.") + .required("OPC is required"), key: Yup.string() - .length(32) - .matches(/^[A-Za-z0-9]+$/, { - message: "Only alphanumeric characters are allowed.", - }) - .required("Key must be a 32 character hexadecimal"), + .length(32, "Key must be 32 hexadecimal characters" ) + .matches(/^[A-Za-z0-9]+$/, "Use valid hexadecimal characters.") + .required("Key is required"), sequenceNumber: Yup.string().required("Sequence number is required"), - deviceGroup: Yup.string().required(""), + selectedSlice: Yup.string().required("Network Slice selection is required"), + deviceGroup: Yup.string().required("Device Group selection is required"), }); - const modalTitle = () => { - return subscriber && rawIMSI ? ("Edit Subscriber: " + rawIMSI) : "Create Subscriber" - } - - const buttonText = () => { - return subscriber ? "Save Changes" : "Create" - } + const modalTitle = subscriber && rawIMSI ? `Edit Subscriber: ${rawIMSI}` : "Create Subscriber"; + const buttonText = subscriber ? "Save Changes" : "Create"; const formik = useFormik({ initialValues: { @@ -98,9 +93,9 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm opc: values.opc, key: values.key, sequenceNumber: values.sequenceNumber, - oldDeviceGroupName: oldDeviceGroupName, + oldDeviceGroupName, newDeviceGroupName: values.deviceGroup, - token: auth.user ? auth.user.authToken : "" + token, }); } else { await createSubscriber({ @@ -109,7 +104,7 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm key: values.key, sequenceNumber: values.sequenceNumber, deviceGroupName: values.deviceGroup, - token: auth.user ? auth.user.authToken : "" + token, }); } await queryClient.invalidateQueries({ queryKey: [queryKeys.subscribers] }); @@ -126,47 +121,33 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm }); const handleSliceChange = (e: React.ChangeEvent) => { - void formik.setFieldValue("selectedSlice", e.target.value); + const selectedSliceName = e.target.value; + formik.setFieldValue("selectedSlice", selectedSliceName); + + const selectedSlice = slices.find((slice) => slice["slice-name"] === selectedSliceName); + if (selectedSlice?.["site-device-group"]?.length === 1) { + formik.setFieldValue("deviceGroup", selectedSlice["site-device-group"][0]); + } else { + formik.setFieldValue("deviceGroup", ""); + } }; const handleDeviceGroupChange = (e: React.ChangeEvent) => { - void formik.setFieldValue("deviceGroup", e.target.value); + formik.setFieldValue("deviceGroup", e.target.value); }; - const selectedSlice = slices.find( - (slice) => slice["slice-name"] === formik.values.selectedSlice, - ); - - const setDeviceGroup = useCallback( - (deviceGroup: string) => { - if (formik.values.deviceGroup !== deviceGroup) { - formik.setFieldValue("deviceGroup", deviceGroup); - } - }, - [formik], - ); - const deviceGroupOptions = React.useMemo(() => { - return selectedSlice && selectedSlice["site-device-group"] - ? selectedSlice["site-device-group"] - : []; - }, [selectedSlice]); + const selectedSlice = slices.find( + (slice) => slice["slice-name"] === formik.values.selectedSlice, + ); + return selectedSlice?.["site-device-group"] || []; + }, [formik.values.selectedSlice, slices]); - useEffect(() => { - if (subscriber && selectedSlice && oldNetworkSliceName === selectedSlice["slice-name"]) { - setDeviceGroup(oldDeviceGroupName); - } - else if (selectedSlice && selectedSlice["site-device-group"]?.length === 1) { - setDeviceGroup(selectedSlice["site-device-group"][0]); - } else { - setDeviceGroup(""); - } - }, [subscriber, selectedSlice, oldNetworkSliceName, oldDeviceGroupName, setDeviceGroup, deviceGroupOptions]); return ( - {buttonText()} + {buttonText} } @@ -243,11 +224,7 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm formik.touched.selectedSlice ? formik.errors.selectedSlice : null } options={[ - { - disabled: true, - label: "Select an option", - value: "", - }, + { disabled: true, label: "Select an option", value: "" }, ...slices.map((slice) => ({ label: slice["slice-name"], value: slice["slice-name"], @@ -263,11 +240,7 @@ const SubscriberModal = ({ toggleModal, subscriber, slices, deviceGroups, onSubm onChange={handleDeviceGroupChange} error={formik.touched.deviceGroup ? formik.errors.deviceGroup : null} options={[ - { - disabled: true, - label: "Select an option", - value: "", - }, + { disabled: true, label: "Select an option", value: "" }, ...deviceGroupOptions.map((group) => ({ label: group, value: group,