diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58ab0a93..514b39d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,7 +110,7 @@ importers: version: 4.0.6(postcss@8.5.16)(tailwindcss@4.3.1) typescript-eslint: specifier: ^8.62.1 - version: 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + version: 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) vite: specifier: ^8.1.3 version: 8.1.3(@types/node@25.9.4)(jiti@2.7.0) @@ -1900,8 +1900,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -3518,40 +3518,40 @@ snapshots: dependencies: vfile-message: 4.0.3 - '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3))(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.62.1 eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 8.62.1 '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 eslint: 10.6.0(jiti@2.7.0) - typescript: 6.0.3 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': + '@typescript-eslint/project-service@8.62.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.6.3) '@typescript-eslint/types': 8.62.1 debug: 4.4.3 - typescript: 6.0.3 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -3560,47 +3560,47 @@ snapshots: '@typescript-eslint/types': 8.62.1 '@typescript-eslint/visitor-keys': 8.62.1 - '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.6.3)': dependencies: - typescript: 6.0.3 + typescript: 5.6.3 - '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) debug: 4.4.3 eslint: 10.6.0(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/project-service': 8.62.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.6.3) '@typescript-eslint/types': 8.62.1 '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(typescript@5.6.3) + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.62.1 '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) eslint: 10.6.0(jiti@2.7.0) - typescript: 6.0.3 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -4589,9 +4589,9 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.5.0(typescript@6.0.3): + ts-api-utils@2.5.0(typescript@5.6.3): dependencies: - typescript: 6.0.3 + typescript: 5.6.3 tslib@2.8.1: {} @@ -4599,18 +4599,18 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3))(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.7.0))(typescript@5.6.3) eslint: 10.6.0(jiti@2.7.0) - typescript: 6.0.3 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - typescript@6.0.3: {} + typescript@5.6.3: {} undici-types@7.24.6: {} diff --git a/src/Components/Auth/With.tsx b/src/Components/Auth/With.tsx index bc2f60c0..71b878d3 100644 --- a/src/Components/Auth/With.tsx +++ b/src/Components/Auth/With.tsx @@ -4,13 +4,14 @@ import { useAuth } from "react-oidc-context"; /** * @author Aloento * @since 1.5.0 - * @version 0.1.0 + * @version 0.1.1 */ export const enum Roles { Readers = "/sd_readers", Creators = "/sd_creators", Operators = "/sd_operators", - Admins = "/sd_admins" + Admins = "/sd_admins", + GitHub = "/status-dashboard" } /** diff --git a/src/Components/Event/Enums.ts b/src/Components/Event/Enums.ts index 1af76eaf..01b17801 100644 --- a/src/Components/Event/Enums.ts +++ b/src/Components/Event/Enums.ts @@ -98,7 +98,7 @@ export function GetStatusList(type: EventType, status?: EventStatus, groups?: st if (groups.some(g => g === Roles.Creators)) { return [EventStatus.PendingReview, EventStatus.Cancelled]; } - if (groups.some(g => g === Roles.Operators || g === Roles.Admins)) { + if (groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub)) { return [EventStatus.PendingReview, ...Object.values(EventStatus).slice(5, 10)]; } } diff --git a/src/Components/Event/EventApprove.tsx b/src/Components/Event/EventApprove.tsx new file mode 100644 index 00000000..acdbc978 --- /dev/null +++ b/src/Components/Event/EventApprove.tsx @@ -0,0 +1,75 @@ +import { Toast, ToastBody, ToastTitle, useToastController } from "@fluentui/react-components"; +import { ScaleButton, ScaleIconActionCheckmark } from "@telekom/scale-components-react"; +import { useRequest } from "ahooks"; +import { useAuth } from "react-oidc-context"; +import { useStatus } from "~/Services/Status"; +import { StatusEnum } from "~/Services/Status.Entities"; +import { Models } from "~/Services/Status.Models"; +import { useAccessToken } from "../Auth/useAccessToken"; +import { EventStatus } from "./Enums"; + +/** + * @author Aloento + * @since 1.5.0 + * @version 0.2.3 + */ +export function EventApprove({ Event }: { Event: Models.IEvent }) { + const { Update } = useStatus(); + + const getToken = useAccessToken(); + const { user } = useAuth(); + const { dispatchToast } = useToastController(); + + const { runAsync, loading } = useRequest(async () => { + const url = process.env.SD_BACKEND_URL!; + const raw = await fetch(`${url}/v2/events/${Event.Id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${getToken()}`, + }, + body: JSON.stringify({ + status: StatusEnum.Reviewed, + version: Event.Version ?? Event.Histories.size + 1, + message: `Approved by ${user?.profile.name || user?.profile.preferred_username}`, + update_date: new Date().toISOString(), + }), + }); + + if (!raw.ok) { + const message = await raw.text(); + dispatchToast( + + Failed to approve event + {message} + , + { intent: "warning" } + ); + throw new Error("Failed to approve event: " + message); + } + + Event.Status = EventStatus.Reviewed; + Event.Histories.add({ + Id: Event.Histories.size + 1, + Created: new Date(), + Event, + Message: `Approved by ${user?.profile.name || user?.profile.preferred_username}`, + Status: EventStatus.Reviewed, + }); + Update(); + }, { + manual: true, + }); + + return ( + runAsync()} + > + +  Approve + + ); +} diff --git a/src/Components/Event/EventCard.tsx b/src/Components/Event/EventCard.tsx index 9e37c77c..0a73463b 100644 --- a/src/Components/Event/EventCard.tsx +++ b/src/Components/Event/EventCard.tsx @@ -4,10 +4,11 @@ import remarkGfm from 'remark-gfm'; import remarkIns from 'remark-ins'; import { Dic } from "~/Helpers/Entities"; import { Models } from "~/Services/Status.Models"; -import { Authorized } from "../Auth/With"; +import { Authorized, Roles } from "../Auth/With"; import { Indicator } from "../Home/Indicator"; import { EventStatus, EventType, IsIncident } from "./Enums"; import { EventAffected } from "./EventAffected"; +import { EventApprove } from "./EventApprove"; import { EventEditor } from "./EventEditor"; import { EventExtract } from "./EventExtract"; @@ -26,7 +27,7 @@ import { EventExtract } from "./EventExtract"; * * @author Aloento * @since 1.0.0 - * @version 0.2.0 + * @version 0.3.1 */ export function EventCard({ Event }: { Event: Models.IEvent }) { return ( @@ -40,15 +41,27 @@ export function EventCard({ Event }: { Event: Models.IEvent }) { - -
- { - Event.RegionServices.size > 1 && - - } +
+ + Event.Status === EventStatus.PendingReview && + groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub)}> + + + + + Event.RegionServices.size > 1 && + groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub)}> + + + + ( + Event.Status === EventStatus.PendingReview && + groups.some(g => g === Roles.Creators) + ) || + groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub)}> -
- + +
diff --git a/src/Components/Event/EventEditor.tsx b/src/Components/Event/EventEditor.tsx index ac344c73..a3f41f19 100644 --- a/src/Components/Event/EventEditor.tsx +++ b/src/Components/Event/EventEditor.tsx @@ -3,6 +3,7 @@ import { useBoolean } from "ahooks"; import dayjs from "dayjs"; import ReactMarkdown from 'react-markdown'; import MdEditor from 'react-markdown-editor-lite'; +import { useAuth } from "react-oidc-context"; import remarkGfm from 'remark-gfm'; import remarkIns from 'remark-ins'; import { Dic, MDDecsPlugins, MDUpdatePlugins } from "~/Helpers/Entities"; @@ -24,16 +25,17 @@ import { useEditForm } from "./useEditForm"; * * @author Aloento * @since 1.0.0 - * @version 0.3.0 + * @version 0.4.1 */ export function EventEditor({ Event }: { Event: Models.IEvent }) { const { State, Actions, Validation, OnSubmit, Loading } = useEditForm(Event); const [open, { setTrue, setFalse }] = useBoolean(); + const auth = useAuth(); return <> - Edit +  Edit setFalse()); }}> - Actions.setType(e.target.value as EventType)} - invalid={!!Validation.type} - helperText={Validation.type} - > - {Object.values(EventType).slice(2, 5).map((type, i) => - - {type} - )} - + { + IsIncident(Event.Type) && + Actions.setType(e.target.value as EventType)} + invalid={!!Validation.type} + helperText={Validation.type} + > + {Object.values(EventType).slice(2, 5).map((type, i) => + + {type} + )} + + } - {GetStatusList(State.type) + {GetStatusList(State.type, Event.Status, (auth.user?.profile as any)?.groups) .map((status, i) => {status} diff --git a/src/Components/Event/EventExtract.tsx b/src/Components/Event/EventExtract.tsx index 44edf1bb..5253bf14 100644 --- a/src/Components/Event/EventExtract.tsx +++ b/src/Components/Event/EventExtract.tsx @@ -7,7 +7,7 @@ import { useEventExtract } from "./useEventExtract"; /** * @author Aloento * @since 1.0.0 - * @version 0.1.0 + * @version 0.2.0 */ export function EventExtract({ Event }: { Event: Models.IEvent }) { const { services, setServices, valServices, OnSubmit, Loading } = useEventExtract(Event); @@ -16,7 +16,7 @@ export function EventExtract({ Event }: { Event: Models.IEvent }) { return <> - Extract Services +  Extract Services { - if (IsIncident(x.Type) && x.End) { + if (x.Status === EventStatus.PendingReview || (IsIncident(x.Type) && x.End)) { return false; } diff --git a/src/Components/Home/ServiceItem.css b/src/Components/Home/ServiceItem.css deleted file mode 100644 index 2c3a6958..00000000 --- a/src/Components/Home/ServiceItem.css +++ /dev/null @@ -1,9 +0,0 @@ -.blue-dot { - position: absolute !important; - top: -10%; - right: -10%; -} - -.with-dot { - position: relative; -} diff --git a/src/Components/Home/ServiceItem.tsx b/src/Components/Home/ServiceItem.tsx index 7e17fd0d..9904cf30 100644 --- a/src/Components/Home/ServiceItem.tsx +++ b/src/Components/Home/ServiceItem.tsx @@ -7,7 +7,7 @@ import { useStatus } from "~/Services/Status"; import { Models } from "~/Services/Status.Models"; import { EventStatus, EventType, IsIncident, IsOpenStatus } from "../Event/Enums"; import { Indicator } from "./Indicator"; -import "./ServiceItem.css"; + import serviceSlugMap from "./serviceSlugMap.json"; interface IServiceItem { diff --git a/src/Components/Layout/MobileMenu.tsx b/src/Components/Layout/MobileMenu.tsx index 8dde1fad..cb39c415 100644 --- a/src/Components/Layout/MobileMenu.tsx +++ b/src/Components/Layout/MobileMenu.tsx @@ -1,14 +1,23 @@ import { ScaleIconActionMenu, ScaleTelekomMobileFlyoutCanvas, ScaleTelekomMobileMenu, ScaleTelekomMobileMenuItem, ScaleTelekomNavFlyout, ScaleTelekomNavItem } from "@telekom/scale-components-react"; +import { chain } from "lodash"; +import { useMemo } from "react"; import { useAuth } from "react-oidc-context"; -import { Authorized } from "../Auth/With"; +import { EventStatus } from "~/Components/Event/Enums"; +import { useStatus } from "~/Services/Status"; +import { Authorized, Roles } from "../Auth/With"; /** * @author Aloento * @since 1.0.0 - * @version 0.1.0 + * @version 0.3.0 */ export function MobileMenu() { const auth = useAuth(); + const { DB } = useStatus(); + + const pendingCount = useMemo(() => chain(DB.Events) + .filter(e => e.Status === EventStatus.PendingReview) + .value().length, [DB]); return ( @@ -39,8 +48,17 @@ export function MobileMenu() { New Event + { + return pendingCount > 0 && + groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub); + }}> + + Reviews: {pendingCount} + + + auth.signoutSilent()}> - Logout {auth.user?.profile.name} + Logout {auth.user?.profile.name || auth.user?.profile.preferred_username} diff --git a/src/Components/Layout/ProfileMenu.tsx b/src/Components/Layout/ProfileMenu.tsx index 7d9ea999..3625a1ab 100644 --- a/src/Components/Layout/ProfileMenu.tsx +++ b/src/Components/Layout/ProfileMenu.tsx @@ -13,21 +13,17 @@ export function ProfileMenu() { - - - +
+ + + +
Hi, {auth.user?.profile.name || auth.user?.profile.preferred_username} - - - New Event - - - auth.signoutSilent()}> Logout diff --git a/src/Components/Layout/TopNavBar.tsx b/src/Components/Layout/TopNavBar.tsx index 2e4225d8..0103b513 100644 --- a/src/Components/Layout/TopNavBar.tsx +++ b/src/Components/Layout/TopNavBar.tsx @@ -1,5 +1,9 @@ import { ScaleTelekomHeader, ScaleTelekomNavItem, ScaleTelekomNavList } from "@telekom/scale-components-react"; +import { chain } from "lodash"; +import { useMemo } from "react"; +import { EventStatus } from "~/Components/Event/Enums"; import { Dic } from "~/Helpers/Entities"; +import { useStatus } from "~/Services/Status"; import { Authorized } from "../Auth/With"; import { MobileMenu } from "./MobileMenu"; import { NavItem } from "./NavItem"; @@ -8,9 +12,15 @@ import { ProfileMenu } from "./ProfileMenu"; /** * @author Aloento * @since 1.0.0 - * @version 0.1.0 + * @version 0.3.0 */ export function TopNavBar() { + const { DB } = useStatus(); + + const pendingCount = useMemo(() => chain(DB.Events) + .filter(e => e.Status === EventStatus.PendingReview) + .value().length, [DB]); + return (
+ + + + + {pendingCount > 0 && } + diff --git a/src/Components/New/NewForm.tsx b/src/Components/New/NewForm.tsx index 0e5d7094..fa81d3df 100644 --- a/src/Components/New/NewForm.tsx +++ b/src/Components/New/NewForm.tsx @@ -3,10 +3,12 @@ import dayjs from "dayjs"; import { orderBy } from "lodash"; import ReactMarkdown from 'react-markdown'; import MdEditor from 'react-markdown-editor-lite'; +import { useAuth } from "react-oidc-context"; import remarkGfm from 'remark-gfm'; import remarkIns from 'remark-ins'; import { Dic, MDDecsPlugins } from "~/Helpers/Entities"; import { useStatus } from "~/Services/Status"; +import { Roles } from "../Auth/With"; import { EventType, IsIncident } from "../Event/Enums"; import { useNewForm } from "./useNewForm"; @@ -25,11 +27,19 @@ import { useNewForm } from "./useNewForm"; * * @author Aloento * @since 1.0.0 - * @version 0.2.0 + * @version 0.2.1 */ export function NewForm() { const { DB } = useStatus(); const { State, Actions, Validation, OnSubmit, Loading } = useNewForm(); + const auth = useAuth(); + + const groups: string[] = (auth.user?.profile as any)?.groups || []; + const isCreatorOnly = !groups.includes(Roles.Operators) && !groups.includes(Roles.Admins) && !groups.includes(Roles.GitHub); + + const availableTypes = isCreatorOnly + ? [EventType.Maintenance] + : Object.values(EventType).slice(1); return ( <> @@ -48,8 +58,9 @@ export function NewForm() { onScale-change={(e) => Actions.setType(e.target.value as EventType)} invalid={!!Validation.type} helperText={Validation.type} + disabled={isCreatorOnly} > - {Object.values(EventType).slice(1).map((type, i) => + {availableTypes.map((type, i) => {type} )} diff --git a/src/Pages/Home.tsx b/src/Pages/Home.tsx index be8455f0..78c06df6 100644 --- a/src/Pages/Home.tsx +++ b/src/Pages/Home.tsx @@ -6,7 +6,8 @@ import { chain } from "lodash"; import { useEffect, useMemo, useState } from "react"; import { Helmet } from "react-helmet"; import { BehaviorSubject, Subject } from "rxjs"; -import { EventType, IsIncident, IsOpenStatus } from "~/Components/Event/Enums"; +import { Authorized, Roles } from "~/Components/Auth/With"; +import { EventStatus, EventType, IsIncident, IsOpenStatus } from "~/Components/Event/Enums"; import { EventGrid } from "~/Components/Home/EventGrid"; import "~/Components/Home/Home.css"; import { Indicator } from "~/Components/Home/Indicator"; @@ -34,7 +35,7 @@ const log = new Logger("Home"); * @component * @author Aloento * @since 1.0.0 - * @version 0.2.0 + * @version 0.3.0 */ export function Home() { const { DB } = useStatus(); @@ -90,12 +91,32 @@ export function Home() { : `${abnormalCount} components have issues, but don't worry, we are working on it.` : "All Systems Operational"; + const pendingCount = useMemo(() => { + const events = chain(DB.Events) + .filter(e => e.Status === EventStatus.PendingReview) + .value(); + + log.debug("Pending Maintenance", events); + return events.length; + }, [DB]); + return ( <> {Dic.Name} {Dic.Prod} + { + return pendingCount > 0 && + groups.some(g => g === Roles.Operators || g === Roles.Admins || g === Roles.GitHub); + }}> + + +
- +
- + {Object.values(EventType).map((state, i) => (
diff --git a/src/Pages/Reviews.tsx b/src/Pages/Reviews.tsx new file mode 100644 index 00000000..f8fd0cfb --- /dev/null +++ b/src/Pages/Reviews.tsx @@ -0,0 +1,123 @@ +import { ScaleDataGrid, ScaleIconActionCheckmark, ScaleIconActionMenu, ScaleMenuFlyoutItem, ScaleMenuFlyoutList } from "@telekom/scale-components-react"; +import dayjs from "dayjs"; +import { chain } from "lodash"; +import { useEffect, useRef, useState } from "react"; +import { Helmet } from "react-helmet"; +import { EventStatus } from "~/Components/Event/Enums"; +import { Dic } from "~/Helpers/Entities"; +import { useStatus } from "~/Services/Status"; + +const PAGE_SIZE_KEY = "reviewsPageSize"; +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + +/** + * @author Aloento + * @since 1.3.0 + * @version 0.3.0 + */ +export function Reviews() { + const { DB } = useStatus(); + const gridRef = useRef(null); + + const [pageSize, setPageSize] = useState(() => { + const stored = localStorage.getItem(PAGE_SIZE_KEY); + return stored ? parseInt(stored, 10) : 10; + }); + + const pendingEvents = DB.Events.filter((x) => x.Status === EventStatus.PendingReview); + + useEffect(() => { + if (!gridRef.current) { + return; + } + + const grid = gridRef.current; + + grid.fields = [ + { type: "number", label: "ID", sortable: true }, + { type: "text", label: "Plan Start CET", sortable: true }, + { type: "text", label: "Plan End CET", sortable: true }, + { type: "text", label: "Region", sortable: true }, + { type: "text", label: "Service", sortable: true, stretchWeight: 0.8 }, + { type: "actions", label: "Detail" }, + ]; + + const events = chain(pendingEvents) + .map((x) => { + const rs = Array.from(x.RegionServices); + + const services = chain(rs) + .map(s => s.Service.Name) + .uniq() + .value(); + + const regions = chain(rs) + .map(r => r.Region.Name) + .uniq() + .value(); + + return [ + x.Id, + dayjs(x.Start).tz(Dic.TZ).format(Dic.Time), + x.End ? dayjs(x.End).tz(Dic.TZ).format(Dic.Time) : "-", + regions.join(", "), + services.length > 2 + ? `${services.slice(0, 2).join(", ")} +${services.length - 2}` + : services.join(", "), + [ + { + label: "↗", + variant: "secondary", + href: `/Event/${x.Id}` + } + ] + ]; + }) + .value(); + + grid.rows = events; + }, [gridRef.current, pendingEvents]); + + return ( + <> + + Reviews - {Dic.Name} {Dic.Prod} + + + + + Page Size + + + + {PAGE_SIZE_OPTIONS.map((size) => ( + { + setPageSize(size); + localStorage.setItem(PAGE_SIZE_KEY, size.toString()); + }} + > + {size} + + + ))} + + + + + ); +} diff --git a/src/Pages/index.tsx b/src/Pages/index.tsx index 50458392..d47baa9a 100644 --- a/src/Pages/index.tsx +++ b/src/Pages/index.tsx @@ -11,6 +11,7 @@ import { Event } from "./Event"; import { History } from "./History"; import { Home } from "./Home"; import { NewEvent } from "./NewEvent"; +import { Reviews } from "./Reviews"; /** * @author Aloento @@ -46,6 +47,9 @@ export function Layout() { case "NewEvent": return ; + case "Reviews": + return ; + case "": case undefined: return ; diff --git a/src/Services/Status.tsx b/src/Services/Status.tsx index 0962de9b..20e9d3fe 100644 --- a/src/Services/Status.tsx +++ b/src/Services/Status.tsx @@ -88,34 +88,40 @@ export function useStatus() { * * @author Aloento * @since 1.0.0 - * @version 0.3.0 + * @version 0.4.0 */ export function StatusContext({ children }: { children: JSX.Element }) { const [ins, setDB] = useState(db.Ins); const auth = useAuth(); - const authRef = useRef(auth); - authRef.current = auth; - + const abortRef = useRef(null); const url = process.env.SD_BACKEND_URL; const { runAsync } = useRequest( async () => { - while (authRef.current.isLoading) { - await new Promise(resolve => setTimeout(resolve, 100)); - } + abortRef.current?.abort(); + abortRef.current = new AbortController(); + const { signal } = abortRef.current; log.info(`Loading status data from v2...`); - const token = authRef.current?.user?.access_token ? - { headers: { Authorization: `Bearer ${authRef.current.user.access_token}` } } : {}; + const token: RequestInit = auth.user?.access_token + ? { + headers: { + Authorization: `Bearer ${auth.user.access_token}` + }, + signal + } + : {}; const compLink = `${url}/v2/components`; const compRes = await fetch(compLink, token); + signal.throwIfAborted(); const compData = await compRes.json(); log.debug("Components Status loaded.", compData); const first = await fetch(`${url}/v2/events?page=1&limit=50`, token); + signal.throwIfAborted(); const firstData = await first.json(); const allEvents: EventEntityV2[] = []; @@ -145,6 +151,7 @@ export function StatusContext({ children }: { children: JSX.Element }) { const remainingPages = await Promise.all(pagePromises); remainingPages.forEach(pageData => { + signal.throwIfAborted(); if (Array.isArray(pageData)) { allEvents.push(...pageData); } @@ -161,18 +168,16 @@ export function StatusContext({ children }: { children: JSX.Element }) { { cacheKey: key, onSuccess: (res) => update(TransformerV2(res)), + onFinally: () => subRef.current?.next(new Date()), + refreshDeps: [auth.user?.access_token], + pollingInterval: 60000, + pollingWhenHidden: false, } ); + const subRef = useRef>(null); useMount(() => { - const sub = Station.get>("Update", () => new Subject()); - - const interval = setInterval(() => { - runAsync(); - sub.next(new Date()); - }, 60000); - - return () => clearInterval(interval); + subRef.current = Station.get>("Update", () => new Subject()); }); function update(data: IStatusContext = ins) { diff --git a/src/index.css b/src/index.css index 4457b7c5..8adb7006 100644 --- a/src/index.css +++ b/src/index.css @@ -15,3 +15,13 @@ img { image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } + +.blue-dot { + position: absolute !important; + top: -10%; + right: -10%; +} + +.with-dot { + position: relative; +}