diff --git a/src/Components/Auth/UserMgr.ts b/src/Components/Auth/UserMgr.ts index 20358a9..7bde0f1 100644 --- a/src/Components/Auth/UserMgr.ts +++ b/src/Components/Auth/UserMgr.ts @@ -51,6 +51,8 @@ export class UserMgr extends UserManager { const state = btoa(stateObj).replace(/=+$/, ''); const loginUrl = `${process.env.SD_BACKEND_URL}/auth/login?state=${state}`; + const current = window.location.href; + await this.settings.userStore.set("current", current); window.location.href = loginUrl; } @@ -89,6 +91,9 @@ export class UserMgr extends UserManager { }); await this.storeUser(user); + this.settings.userStore.get("current").then((current) => { + window.location.href = current || "/"; + }); return user; } } diff --git a/src/Components/Availability/AvailaMatrix.tsx b/src/Components/Availability/AvailaMatrix.tsx index e468804..7fb5e64 100644 --- a/src/Components/Availability/AvailaMatrix.tsx +++ b/src/Components/Availability/AvailaMatrix.tsx @@ -1,11 +1,7 @@ import { ScaleTable } from "@telekom/scale-components-react"; -import { useCreation } from "ahooks"; import dayjs from "dayjs"; import { chain } from "lodash"; -import { useEffect, useState } from "react"; -import { BehaviorSubject } from "rxjs"; -import { Station } from "~/Helpers/Entities"; -import { useStatus } from "~/Services/Status"; +import { useAvailability } from "~/Services/Availability"; import { CategoryGroup } from "./CategoryGroup"; /** @@ -14,20 +10,7 @@ import { CategoryGroup } from "./CategoryGroup"; * @version 0.1.0 */ export function AvailaMatrix() { - const { DB } = useStatus(); - const [region, setRegion] = useState(DB.Regions[0]); - - const topic = "Availability"; - const regionSub = useCreation( - () => Station.get(topic, () => { - const first = DB.Regions[0]; - return new BehaviorSubject(first); - }), []); - - useEffect(() => { - const sub = regionSub.subscribe(setRegion); - return () => sub.unsubscribe(); - }, []); + const { Region } = useAvailability(); return ( @@ -63,11 +46,11 @@ export function AvailaMatrix() { - {chain(Array.from(region.Services)) + {chain(Array.from(Region.Services)) .map(x => x.Category) .uniqBy(x => x.Id) .orderBy(x => x.Name) - .map((x, i) => ) + .map((x, i) => ) .value()} diff --git a/src/Components/Availability/CategoryGroup.tsx b/src/Components/Availability/CategoryGroup.tsx index 51e6b07..d157444 100644 --- a/src/Components/Availability/CategoryGroup.tsx +++ b/src/Components/Availability/CategoryGroup.tsx @@ -1,18 +1,22 @@ +import { useMemo } from "react"; +import { useAvailability } from "~/Services/Availability"; import { Models } from "~/Services/Status.Models"; -import { useAvailability } from "./useAvailability"; - -interface ICategoryGroup { - Category: Models.ICategory; - Topic: string; -} /** * @author Aloento * @since 1.0.0 * @version 0.1.0 */ -export function CategoryGroup({ Category, Topic }: ICategoryGroup) { - const avas = useAvailability(Category, Topic); +export function CategoryGroup({ Category }: { Category: Models.ICategory }) { + const { Availa, Region } = useAvailability(); + + const avas = useMemo(() => { + const res = Availa + .filter(x => x.RS.Region.Id === Region.Id) + .filter(x => x.RS.Service.Category.Id === Category.Id); + + return res; + }, [Availa, Region]); function getColor(val: number): string { const color = val >= 99.95 diff --git a/src/Components/Availability/useAvailability.ts b/src/Components/Availability/useAvailability.ts deleted file mode 100644 index 8fbd5cd..0000000 --- a/src/Components/Availability/useAvailability.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { useCreation, useRequest } from "ahooks"; -import { useEffect, useState } from "react"; -import { BehaviorSubject } from "rxjs"; -import { Station } from "~/Helpers/Entities"; -import { Logger } from "~/Helpers/Logger"; -import { useStatus } from "~/Services/Status"; -import { Models } from "~/Services/Status.Models"; - -const log = new Logger("useAvailability"); - -interface ServiceAvaEntity { - id: number - name: string - availability: AvailEntity[] - region: string -} - -interface AvailEntity { - year: number - month: number - percentage: number -} - -interface IAvailability { - RS: Models.IRegionService, - Percentages: number[] -} - -/** - * Custom hook to manage availability data based on the provided category and topic. - * This hook interacts with the status service and fetches availability data from a backend service. - * It processes and filters the data to return availability information for a specific region and category. - * - * @param category - The category of the service for which availability data is required. - * @param topic - The topic used to subscribe to region updates. - * @returns An array of availability information objects. - * - * @remarks - * This hook uses various state and effect hooks to manage and update the availability data. - * It also utilizes a custom logger for debugging purposes. - * The data fetching is performed using a request hook with caching enabled. - * - * @author Aloento - * @since 1.0.0 - * @version 0.1.0 - */ -export function useAvailability(category: Models.ICategory, topic: string) { - const { DB } = useStatus(); - - const [region, setRegion] = useState(DB.Regions[0]); - const regionSub = useCreation( - () => Station.get>(topic), []); - - useEffect(() => { - const sub = regionSub.subscribe(setRegion); - return () => sub.unsubscribe(); - }, []); - - const [avas, setAvas] = useState( - () => DB.RegionService - .filter(x => x.Region.Id === region.Id) - .filter(x => x.Service.Category.Id === category.Id) - .map(x => ({ - RS: x, - Percentages: Array(6).fill(100) - }))); - - const url = process.env.SD_BACKEND_URL; - - const { data } = useRequest(async () => { - const res = await fetch(`${url}/availability`); - const data = (await res.json()).data as ServiceAvaEntity[]; - - const raw = [] as IAvailability[]; - - for (const service of data) { - const rs = DB.RegionService.find(x => x.Id === service.id); - - if (!rs) { - log.info("Service not found.", service); - continue; - } - - const ava = service.availability - .map(x => x.percentage) - .slice(0, 6) - .reverse(); - - raw.push({ - RS: rs, - Percentages: ava - }); - } - - log.debug("Availability data processed.", raw); - return raw; - }, { - cacheKey: log.namespace - }); - - useEffect(() => { - if (data) { - const res = data - .filter(x => x.RS.Region.Id === region.Id) - .filter(x => x.RS.Service.Category.Id === category.Id); - - setAvas(res); - } - }, [data, category, region]); - - return avas; -} diff --git a/src/Components/New/useNewForm.ts b/src/Components/New/useNewForm.ts index aafaf33..7d88c5b 100644 --- a/src/Components/New/useNewForm.ts +++ b/src/Components/New/useNewForm.ts @@ -179,7 +179,7 @@ export function useNewForm() { start_date: start.toISOString() } - if (type === EventType.Maintenance) { + if (type === EventType.Maintenance && end) { body.end_date = end } diff --git a/src/Pages/Availability.tsx b/src/Pages/Availability.tsx index 70d62cf..1d3605e 100644 --- a/src/Pages/Availability.tsx +++ b/src/Pages/Availability.tsx @@ -1,6 +1,7 @@ import { Helmet } from "react-helmet"; import { AvailaMatrix } from "~/Components/Availability/AvailaMatrix"; import { RegionSelector } from "~/Components/Home/RegionSelector"; +import { AvailaContext } from "~/Services/Availability"; /** * @author Aloento @@ -18,6 +19,8 @@ export function Availability() { Topic="Availability" /> - + + + ; } diff --git a/src/Services/Availability.tsx b/src/Services/Availability.tsx new file mode 100644 index 0000000..d49ad07 --- /dev/null +++ b/src/Services/Availability.tsx @@ -0,0 +1,135 @@ +import { ScaleLoadingSpinner } from "@telekom/scale-components-react"; +import { useCreation, useRequest } from "ahooks"; +import { createContext, JSX, Suspense, useContext, useEffect, useState } from "react"; +import { BehaviorSubject } from "rxjs"; +import { Station } from "~/Helpers/Entities"; +import { Logger } from "~/Helpers/Logger"; +import { DB } from "./DB"; +import { useStatus } from "./Status"; +import { Models } from "./Status.Models"; + +interface AvailEntity { + year: number + month: number + percentage: number +} + +interface ServiceAvaEntity { + id: number + name: string + availability: AvailEntity[] + region: string +} + +interface IAvailability { + RS: Models.IRegionService, + Percentages: number[] +} + +interface IContext { + Availa: IAvailability[]; + Region: Models.IRegion +} + +const db = new DB(() => []); + +const CTX = createContext({} as IContext); +const key = "Availability"; + +await db.load(key); + +const log = new Logger("Service", key); + +/** + * @author Aloento + * @since 1.0.0 + * @version 0.1.0 + */ +export function useAvailability() { + const ctx = useContext(CTX); + + if (db.Ins.length < 1) { + throw new Promise((res) => { + const i = setInterval(() => { + if (db.Ins.length > 0) { + clearInterval(i); + res(ctx); + } + }, 100); + }); + } + + return ctx; +} + +/** + * @author Aloento + * @since 1.0.0 + * @version 0.1.0 + */ +export function AvailaContext({ children }: { children: JSX.Element }) { + const { DB } = useStatus(); + const [region, setRegion] = useState(DB.Regions[0]); + const [ins, setDB] = useState(db.Ins); + + const regionSub = useCreation( + () => Station.get(key, () => { + const first = DB.Regions[0]; + return new BehaviorSubject(first); + }), []); + + useEffect(() => { + const sub = regionSub.subscribe(setRegion); + return () => sub.unsubscribe(); + }, []); + + const url = process.env.SD_BACKEND_URL; + + useRequest(async () => { + const res = await fetch(`${url}/v2/availability`); + const data = (await res.json()).data as ServiceAvaEntity[]; + + const raw = [] as IAvailability[]; + + for (const service of data) { + const rs = DB.RegionService.find(x => x.Id === service.id); + + if (!rs) { + log.info("Service not found.", service); + continue; + } + + if (!service.availability || service.availability.length < 6) { + log.info(`Skipped ${key}.`, service); + continue; + } + + const ava = service.availability + .map(x => x.percentage) + .slice(0, 6) + .reverse(); + + raw.push({ + RS: rs, + Percentages: ava + }); + } + + log.debug(`${key} data processed.`, raw); + return raw; + }, { + cacheKey: key, + onSuccess: (res) => { + setDB(res); + db.save(key, res); + } + }); + + return ( + + }> + {children} + + + ); +} diff --git a/src/Services/DB.ts b/src/Services/DB.ts new file mode 100644 index 0000000..a5607b0 --- /dev/null +++ b/src/Services/DB.ts @@ -0,0 +1,39 @@ +import { openDB } from "idb"; +import { Dic } from "~/Helpers/Entities"; + +/** + * @author Aloento + * @since 1.0.0 + * @version 0.1.0 + */ +export class DB { + public Ins: T; + + constructor(factory: () => T) { + this.Ins = factory(); + } + + public async init() { + return openDB(Dic.Name, 1, { + upgrade(db) { + db.createObjectStore(Dic.Name); + }, + }); + } + + public async save(key: string, data = this.Ins) { + this.Ins = data; + const db = await this.init(); + await db.put(Dic.Name, data, key); + db.close(); + } + + public async load(key: string) { + const db = await this.init(); + const res = await db.get(Dic.Name, key) as T; + if (res) { + this.Ins = res; + } + db.close(); + } +} diff --git a/src/Services/Status.tsx b/src/Services/Status.tsx index b1fba4b..d335ecc 100644 --- a/src/Services/Status.tsx +++ b/src/Services/Status.tsx @@ -1,8 +1,7 @@ import { useRequest } from "ahooks"; -import { openDB } from "idb"; -import { createContext, useContext, useState } from "react"; -import { Dic } from "~/Helpers/Entities"; +import { createContext, JSX, useContext, useState } from "react"; import { Logger } from "~/Helpers/Logger"; +import { DB } from "./DB"; import { IncidentEntityV2, StatusEntityV2 } from "./Status.Entities"; import { IStatusContext } from "./Status.Models"; import { TransformerV2 } from "./Status.Trans.V2"; @@ -16,12 +15,6 @@ import { TransformerV2 } from "./Status.Trans.V2"; * This function sets up the initial structure for the status database context. * It is used to ensure that the database has a consistent structure before any data is loaded. * - * @example - * ```typescript - * const initialContext = EmptyDB(); - * console.log(initialContext.Services); // Outputs: [] - * ``` - * * @author Aloento * @since 1.0.0 * @version 0.1.0 @@ -36,18 +29,7 @@ export function EmptyDB(): IStatusContext { } } -/** - * A global variable representing the current state of the status database. - * - * @remarks - * This variable is initialized with the structure provided by the `EmptyDB` function. - * It is used throughout the application to access and update the status data. - * - * @author Aloento - * @since 1.0.0 - * @version 0.1.0 - */ -export let DB = EmptyDB(); +const db = new DB(EmptyDB); interface IContext { DB: IStatusContext; @@ -55,82 +37,11 @@ interface IContext { } const CTX = createContext({} as IContext); -const Store = "Status"; - -/** - * Initializes the IndexedDB database for storing status information. - * - * @returns A promise that resolves to the initialized database instance. - * - * @remarks - * This function sets up the IndexedDB database with the necessary object stores. - * It is called internally to ensure the database is ready for use. - * - * @since 1.0.0 - * @version 0.1.0 - */ -function init() { - return openDB(Dic.Name, 1, { - upgrade(db) { - db.createObjectStore(Store); - }, - }); -} +const key = "Status"; -/** - * Saves the current state of the status database to IndexedDB. - * - * @returns A promise that resolves when the save operation is complete. - * - * @remarks - * This function writes the current state of the `DB` variable to the IndexedDB database. - * It is called whenever the status data is updated to persist the changes. - * - * @since 1.0.0 - * @version 0.1.0 - */ -async function save() { - const db = await init(); - await db.put(Store, DB, Store); - db.close(); -} - -/** - * Loads the status database from IndexedDB. - * - * @returns A promise that resolves when the load operation is complete. - * - * @remarks - * This function reads the status data from the IndexedDB database and updates the `DB` variable. - * It is called during the initialization of the application to restore the previous state. - * - * @since 1.0.0 - * @version 0.1sv.0 - */ -async function load() { - const db = await init(); - const res = await db.get(Store, Store) as IStatusContext; - if (res) { - DB = res; - } - db.close(); -} - -/** - * Loads the status database from IndexedDB. - * - * @returns {Promise} A promise that resolves when the load operation is complete. - * - * @remarks - * This function reads the status data from the IndexedDB database and updates the `DB` variable. - * It is called during the initialization of the application to restore the previous state. - * - * @since 1.0.0 - * @version 0.1sv.0 - */ -await load(); +await db.load(key); -const log = new Logger("Service", "Status"); +const log = new Logger("Service", key); /** * Custom hook to access the status context. @@ -149,10 +60,10 @@ const log = new Logger("Service", "Status"); export function useStatus() { const ctx = useContext(CTX); - if (DB.Regions.length < 1) { + if (db.Ins.Regions.length < 1) { throw new Promise((res) => { const i = setInterval(() => { - if (DB.Regions.length > 0) { + if (db.Ins.Regions.length > 0) { clearInterval(i); res(ctx); } @@ -180,7 +91,7 @@ export function useStatus() { * @version 0.1.0 */ export function StatusContext({ children }: { children: JSX.Element }) { - const [db, setDB] = useState(DB); + const [ins, setDB] = useState(db.Ins); const url = process.env.SD_BACKEND_URL; @@ -206,18 +117,18 @@ export function StatusContext({ children }: { children: JSX.Element }) { }; }, { - cacheKey: log.namespace, + cacheKey: key, onSuccess: (res) => update(TransformerV2(res)), } ); - function update(data: IStatusContext = DB) { - DB = { ...data }; - setDB(DB); - save(); + function update(data: IStatusContext = ins) { + const raw = { ...data }; + setDB(raw); + db.save(key, raw); } return ( - {children} + {children} ); }