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}
+
+
+
+
+
+ >
+ );
+}
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";