diff --git a/src/Components/Auth/With.tsx b/src/Components/Auth/With.tsx index 1a94183..bb6642f 100644 --- a/src/Components/Auth/With.tsx +++ b/src/Components/Auth/With.tsx @@ -9,7 +9,7 @@ import { useAuth } from "react-oidc-context"; export function Authorized({ children }: { children: ReactNode }): ReactNode { const auth = useAuth(); - if (auth.isAuthenticated) { + if (auth.isAuthenticated || process.env.NODE_ENV === "development") { return children; } diff --git a/src/Components/Auth/index.tsx b/src/Components/Auth/index.tsx index e42a3db..1f36a8b 100644 --- a/src/Components/Auth/index.tsx +++ b/src/Components/Auth/index.tsx @@ -36,7 +36,8 @@ function AuthHandler() { useMount(() => { if (Paths.at(0) === "signin-oidc") { - return userMgr.signinCallback(); + userMgr.signinCallback(); + return } if (Paths.at(0) === "signout-callback-oidc") { diff --git a/src/Components/Event/EventCard.tsx b/src/Components/Event/EventCard.tsx index 58aac46..c3a4a05 100644 --- a/src/Components/Event/EventCard.tsx +++ b/src/Components/Event/EventCard.tsx @@ -72,6 +72,13 @@ export function EventCard({ Event }: { Event: Models.IEvent }) { } + + + {Event.ContactEmail && + } +
@@ -95,6 +102,15 @@ export function EventCard({ Event }: { Event: Models.IEvent }) { } + + + {Event.ContactEmail && + } +
diff --git a/src/Components/Event/EventEditor.tsx b/src/Components/Event/EventEditor.tsx index 0bea011..705f1fe 100644 --- a/src/Components/Event/EventEditor.tsx +++ b/src/Components/Event/EventEditor.tsx @@ -20,7 +20,7 @@ import { useEditForm } from "./useEditForm"; * * @author Aloento * @since 1.0.0 - * @version 0.3.0 + * @version 0.4.0 */ export function EventEditor({ Event }: { Event: Models.IEvent }) { const { State, Actions, Validation, OnSubmit, Loading } = useEditForm(Event); @@ -125,6 +125,18 @@ export function EventEditor({ Event }: { Event: Models.IEvent }) { helperText={Validation.description} /> + {State.type === EventType.Maintenance && ( + Actions.setContactEmail(e.target.value as string)} + invalid={!!Validation.contactEmail} + helperText={Validation.contactEmail} + /> + )} + (); + function setContactEmail(value = contactEmail) { + let err: boolean = false; + + if (type === EventType.Maintenance && !value) { + setValContactEmail("Contact Email is required for maintenance."); + err = true; + } + + if (value && !value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + setValContactEmail("Please enter a valid email address."); + err = true; + } + + if (value && value.length > 100) { + setValContactEmail("Email must be less than 100 characters."); + err = true; + } + + _setContactEmail(value); + !err && setValContactEmail(undefined); + + return !err; + } + const [status, _setStatus] = useState(); const [valStatus, setValStatus] = useState(); function setStatus(value = status) { @@ -188,7 +214,7 @@ export function useEditForm(event: Models.IEvent) { const { DB, Update } = useStatus(); const { runAsync, loading } = useRequest(async () => { - if (![setTitle(), setType(), setUpdate(), setDescription(), setStatus(), setStart(), setEnd(), setUpdateAt()].every(Boolean)) { + if (![setTitle(), setType(), setUpdate(), setDescription(), setContactEmail(), setStatus(), setStart(), setEnd(), setUpdateAt()].every(Boolean)) { throw new Error("Validation failed."); } const url = process.env.SD_BACKEND_URL!; @@ -202,6 +228,10 @@ export function useEditForm(event: Models.IEvent) { description, }; + if (type === EventType.Maintenance && contactEmail) { + body.contact_email = contactEmail; + }; + if (event.Type !== type) { body.status = StatusEnum.ImpactChanged; } @@ -246,6 +276,7 @@ export function useEditForm(event: Models.IEvent) { updatedEvent.Start = start; updatedEvent.End = end; updatedEvent.Description = description; + updatedEvent.ContactEmail = contactEmail; const newHistory: Models.IHistory = { Id: Math.max(...Array.from(updatedEvent.Histories).map(h => h.Id), 0) + 1, @@ -273,6 +304,7 @@ export function useEditForm(event: Models.IEvent) { type, update, description, + contactEmail, status, start, end, @@ -283,6 +315,7 @@ export function useEditForm(event: Models.IEvent) { setType, setUpdate, setDescription, + setContactEmail, setStatus, setStart, setEnd, @@ -293,6 +326,7 @@ export function useEditForm(event: Models.IEvent) { type: valType, update: valUpdate, description: valDescription, + contactEmail: valContactEmail, status: valStatus, start: valStart, end: valEnd, diff --git a/src/Components/Home/serviceSlugMap.json b/src/Components/Home/serviceSlugMap.json index 900d895..f968de5 100644 --- a/src/Components/Home/serviceSlugMap.json +++ b/src/Components/Home/serviceSlugMap.json @@ -12,6 +12,10 @@ "Open Telekom Cloud": "https://www.open-telekom-cloud.com/", "Open Telekom Cloud Community": "https://community.open-telekom-cloud.com/", "Open Telekom Cloud Console": "https://console.otc.t-systems.com/", + "T Cloud Public Homepage": "https://public.t-cloud.com/", + "T Cloud Public Community": "https://community.open-telekom-cloud.com/", + "T Cloud Public Console": "https://console.otc.t-systems.com/", + "MyWorkplace": "https://myworkplace.t-systems.com/MyWorkplace/Login.aspx", "Financial Dashboard": "", "Invoicing": "" } diff --git a/src/Components/Layout/PageFooter.tsx b/src/Components/Layout/PageFooter.tsx index 7b84bff..5bb63cc 100644 --- a/src/Components/Layout/PageFooter.tsx +++ b/src/Components/Layout/PageFooter.tsx @@ -9,19 +9,19 @@ export function PageFooter() { return ( - © Deutsche Telekom AG + © T-Systems International GmbH diff --git a/src/Components/Layout/ProfileMenu.tsx b/src/Components/Layout/ProfileMenu.tsx index 2d5bbae..9a35c6f 100644 --- a/src/Components/Layout/ProfileMenu.tsx +++ b/src/Components/Layout/ProfileMenu.tsx @@ -1,5 +1,7 @@ +import { ODSBadgeNumber } from "@telekom-ods/react-ui-kit"; import { ScaleButton, ScaleIconUserFileUser, ScaleMenuFlyout, ScaleMenuFlyoutItem, ScaleMenuFlyoutList, ScaleTelekomNavItem } from "@telekom/scale-components-react"; import { useAuth } from "react-oidc-context"; +import { Authorized } from "../Auth/With"; /** * @author Aloento @@ -13,9 +15,21 @@ export function ProfileMenu() { - - - +
+ + + + + +
+ +
+
+
@@ -23,11 +37,17 @@ export function ProfileMenu() { - + New Event + + + Reviews + + + auth.signoutSilent()}> Logout diff --git a/src/Components/New/NewForm.tsx b/src/Components/New/NewForm.tsx index 0767e5d..c0f1fbf 100644 --- a/src/Components/New/NewForm.tsx +++ b/src/Components/New/NewForm.tsx @@ -21,7 +21,7 @@ import { useNewForm } from "./useNewForm"; * * @author Aloento * @since 1.0.0 - * @version 0.1.0 + * @version 0.3.0 */ export function NewForm() { const { DB } = useStatus(); @@ -147,6 +147,19 @@ export function NewForm() { /> )} + {State.type === EventType.Maintenance && ( + Actions.setContactEmail(e.target.value as string)} + invalid={!!Validation.contactEmail} + helperText={Validation.contactEmail} + /> + )} + (); + function setContactEmail(value = contactEmail) { + let err: boolean = false; + + if (type === EventType.Maintenance && !value) { + setValContactEmail("Contact Email is required for maintenance."); + err = true; + } + + if (value && !value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + setValContactEmail("Please enter a valid email address."); + err = true; + } + + if (value && value.length > 100) { + setValContactEmail("Email must be less than 100 characters."); + err = true; + } + + _setContactEmail(value); + !err && setValContactEmail(undefined); + + return !err; + } + const [services, _setServices] = useState([]); const [valServices, setValServices] = useState(); function setServices(action: (curr: Models.IRegionService[]) => Models.IRegionService[] = (s) => s) { @@ -150,7 +176,7 @@ export function useNewForm() { const getToken = useAccessToken(); const { runAsync, loading } = useRequest(async () => { - if (![setTitle(), setType(), setDescription(), setStart(), setEnd(), setServices()].every(Boolean)) { + if (![setTitle(), setType(), setDescription(), setStart(), setEnd(), setServices(), setContactEmail()].every(Boolean)) { return; } @@ -180,6 +206,10 @@ export function useNewForm() { start_date: start.toISOString() } + if (type === EventType.Maintenance && contactEmail) { + body.contact_email = contactEmail; + } + if (!IsIncident(type) && end) { body.end_date = end } @@ -215,7 +245,8 @@ export function useNewForm() { description, start, end, - services + services, + contactEmail }, Actions: { setTitle, @@ -223,7 +254,8 @@ export function useNewForm() { setDescription, setStart, setEnd, - setServices + setServices, + setContactEmail }, Validation: { title: valTitle, @@ -231,7 +263,8 @@ export function useNewForm() { description: valDescription, start: valStart, end: valEnd, - services: valServices + services: valServices, + contactEmail: valContactEmail }, OnSubmit: runAsync, Loading: loading diff --git a/src/Pages/Home.tsx b/src/Pages/Home.tsx index be8455f..dfc7710 100644 --- a/src/Pages/Home.tsx +++ b/src/Pages/Home.tsx @@ -96,6 +96,12 @@ export function Home() { {Dic.Name} {Dic.Prod} + + (null); + + const [pageSize, setPageSize] = useState(() => { + const stored = localStorage.getItem(PAGE_SIZE_KEY); + return stored ? parseInt(stored, 10) : 10; + }); + + interface ReviewItem { + id: number; + planStartCET: string; + planEndCET: string; + region: string; + service: string; + } + + const historyItems = reviewsHistoryMock as ReviewItem[]; + + 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 = historyItems.map((item) => [ + item.id, + item.planStartCET, + item.planEndCET, + item.region, + item.service, + [ + { + label: "↗", + variant: "secondary", + href: `/Event/${item.id}` + } + ] + ]); + + grid.rows = events; + }, [gridRef.current, historyItems]); + + 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 8eef76d..33391c3 100644 --- a/src/Pages/index.tsx +++ b/src/Pages/index.tsx @@ -11,11 +11,12 @@ import { Event } from "./Event"; import { History } from "./History"; import { Home } from "./Home"; import { NewEvent } from "./NewEvent"; +import { Reviews } from "./Reviews"; /** * @author Aloento * @since 1.0.0 - * @version 0.1.0 + * @version 0.2.0 */ export function Layout() { const { Paths } = useRouter(); @@ -49,6 +50,9 @@ export function Layout() { case "NewEvent": return ; + case "Reviews": + return ; + case "": case undefined: return ; diff --git a/src/Pages/reviewsHistoryMock.json b/src/Pages/reviewsHistoryMock.json new file mode 100644 index 0000000..9d63e0e --- /dev/null +++ b/src/Pages/reviewsHistoryMock.json @@ -0,0 +1,163 @@ +[ + { + "id": 701, + "planStartCET": "2026-02-01 09:30:00", + "planEndCET": "2026-02-01 12:45:00", + "region": "EU-DE, EU-NL", + "service": "Compute, Storage" + }, + { + "id": 702, + "planStartCET": "2026-02-02 14:00:00", + "planEndCET": "2026-02-02 18:00:00", + "region": "EU-DE, EU-NL", + "service": "Network" + }, + { + "id": 703, + "planStartCET": "2026-02-03 08:15:00", + "planEndCET": "-", + "region": "EU-NL", + "service": "Database, API Gateway +2" + }, + { + "id": 704, + "planStartCET": "2026-01-30 22:00:00", + "planEndCET": "2026-01-31 02:30:00", + "region": "EU-DE", + "service": "Object Storage" + }, + { + "id": 705, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 706, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 707, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 708, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 709, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 710, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 711, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 712, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 713, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 714, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 715, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 716, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 717, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 718, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 719, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 720, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 721, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 722, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + }, + { + "id": 723, + "planStartCET": "2026-01-28 16:00:00", + "planEndCET": "2026-01-28 20:00:00", + "region": "EU-DE", + "service": "Load Balancer" + } +] diff --git a/src/Services/Status.Models.ts b/src/Services/Status.Models.ts index 87c052d..f5f19e2 100644 --- a/src/Services/Status.Models.ts +++ b/src/Services/Status.Models.ts @@ -54,6 +54,7 @@ export namespace Models { End?: Date; Status: EventStatus; Description?: string; + ContactEmail?: string; RegionServices: Set; Histories: Set; } diff --git a/src/index.tsx b/src/index.tsx index 3b74739..71ee290 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1,4 @@ +import "@telekom-ods/react-ui-kit/style.css"; import "@telekom/scale-components/dist/scale-components/scale-components.css"; import "./index.css";