diff --git a/farm.config.ts b/farm.config.ts
index dc26d53..d83740e 100644
--- a/farm.config.ts
+++ b/farm.config.ts
@@ -33,7 +33,8 @@ export default defineConfig((cfg) => {
server: {
proxy: {
"/auth": {
- target: "http://localhost:8000",
+ // target: "http://localhost:8000",
+ target: "https://api.test.status.otc-service.com",
changeOrigin: true
},
"/v2": {
diff --git a/package.json b/package.json
index 0f79b92..9f90810 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "statusdashboard",
- "version": "1.4.0",
+ "version": "1.5.0",
"type": "module",
"scripts": {
"dev": "farm start",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 747127e..4295962 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1106,9 +1106,6 @@ packages:
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
- '@types/node@25.6.2':
- resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==}
-
'@types/node@25.7.0':
resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==}
@@ -2252,8 +2249,8 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
- node-releases@2.0.38:
- resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
+ node-releases@2.0.44:
+ resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
@@ -2679,9 +2676,6 @@ packages:
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
hasBin: true
- undici-types@7.19.2:
- resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
-
undici-types@7.21.0:
resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==}
@@ -4467,7 +4461,7 @@ snapshots:
'@types/http-proxy@1.17.17':
dependencies:
- '@types/node': 25.6.2
+ '@types/node': 25.7.0
'@types/js-cookie@3.0.6': {}
@@ -4477,10 +4471,6 @@ snapshots:
'@types/node@12.20.55': {}
- '@types/node@25.6.2':
- dependencies:
- undici-types: 7.19.2
-
'@types/node@25.7.0':
dependencies:
undici-types: 7.21.0
@@ -4699,7 +4689,7 @@ snapshots:
baseline-browser-mapping: 2.10.29
caniuse-lite: 1.0.30001792
electron-to-chromium: 1.5.353
- node-releases: 2.0.38
+ node-releases: 2.0.44
update-browserslist-db: 1.2.3(browserslist@4.28.2)
buffer@5.7.1:
@@ -5612,7 +5602,7 @@ snapshots:
negotiator@1.0.0: {}
- node-releases@2.0.38: {}
+ node-releases@2.0.44: {}
normalize-path@3.0.0: {}
@@ -5980,8 +5970,6 @@ snapshots:
ua-parser-js@1.0.41: {}
- undici-types@7.19.2: {}
-
undici-types@7.21.0: {}
unist-util-stringify-position@4.0.0:
diff --git a/src/App.tsx b/src/App.tsx
index b3e7bd9..ab2a82d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,3 +1,5 @@
+import { ScaleLoadingSpinner } from "@telekom/scale-components-react";
+import { Suspense } from "react";
import { OIDCProvider } from "./Components/Auth";
import { BrowserRouter } from "./Components/Router";
import { Layout } from "./Pages";
@@ -16,12 +18,14 @@ import { StatusContext } from "./Services/Status";
*/
export function App() {
return (
-
-
-
-
-
-
-
+ }>
+
+
+
+
+
+
+
+
)
}
diff --git a/src/Components/Auth/With.tsx b/src/Components/Auth/With.tsx
index 1a94183..2334261 100644
--- a/src/Components/Auth/With.tsx
+++ b/src/Components/Auth/With.tsx
@@ -4,12 +4,15 @@ import { useAuth } from "react-oidc-context";
/**
* @author Aloento
* @since 1.0.0
- * @version 0.1.0
+ * @version 0.2.0
*/
-export function Authorized({ children }: { children: ReactNode }): ReactNode {
+export function Authorized({ children, rules }: { children: ReactNode, rules?: (groups: string[]) => boolean }): ReactNode {
const auth = useAuth();
- if (auth.isAuthenticated) {
+ if (auth.isAuthenticated || process.env.NODE_ENV === "development") {
+ if (rules && !rules((auth.user?.profile as any)?.groups as string[] || [])) {
+ return null;
+ }
return children;
}
diff --git a/src/Components/Auth/index.tsx b/src/Components/Auth/index.tsx
index a18e676..270dd43 100644
--- a/src/Components/Auth/index.tsx
+++ b/src/Components/Auth/index.tsx
@@ -27,7 +27,7 @@ const log = new Logger("Auth");
/**
* @author Aloento
* @since 1.0.0
- * @version 1.0.0
+ * @version 1.1.0
*/
function AuthHandler() {
const auth = useAuth();
diff --git a/src/Components/Availability/CategoryGroup.tsx b/src/Components/Availability/CategoryGroup.tsx
index d157444..7803dd4 100644
--- a/src/Components/Availability/CategoryGroup.tsx
+++ b/src/Components/Availability/CategoryGroup.tsx
@@ -5,7 +5,7 @@ import { Models } from "~/Services/Status.Models";
/**
* @author Aloento
* @since 1.0.0
- * @version 0.1.0
+ * @version 0.2.0
*/
export function CategoryGroup({ Category }: { Category: Models.ICategory }) {
const { Availa, Region } = useAvailability();
@@ -26,6 +26,10 @@ export function CategoryGroup({ Category }: { Category: Models.ICategory }) {
return `bg-${color}-100 hover:bg-${color}-200`;
}
+ if (!avas.length) {
+ return null;
+ }
+
return <>
diff --git a/src/Components/Event/Enums.ts b/src/Components/Event/Enums.ts
index ea59bff..210bcd7 100644
--- a/src/Components/Event/Enums.ts
+++ b/src/Components/Event/Enums.ts
@@ -64,7 +64,7 @@ export function IsIncident(type: EventType): boolean {
/**
* @author Aloento
* @since 1.0.0
- * @version 0.2.1
+ * @version 0.3.1
*/
export enum EventStatus {
Detected = "Detected",
@@ -82,14 +82,21 @@ export enum EventStatus {
Active = "Active",
Reopened = "Reopened",
Changed = "Changed",
+
+ PendingReview = "Pending Review",
+ Reviewed = "Reviewed",
}
/**
* @author Aloento
* @since 1.1.0
- * @version 0.1.1
+ * @version 0.2.0
*/
-export function GetStatusList(type: EventType): EventStatus[] {
+export function GetStatusList(type: EventType, status?: EventStatus, groups?: string[]): EventStatus[] {
+ if (groups && status === EventStatus.PendingReview && groups.some(g => g === "/sd_creators")) {
+ return [EventStatus.PendingReview, EventStatus.Cancelled];
+ }
+
switch (type) {
case EventType.Maintenance:
return Object.values(EventStatus).slice(5, 10);
@@ -112,7 +119,7 @@ export function IsOpenStatus(status: EventStatus): boolean {
/**
* @author Aloento
* @since 1.0.0
- * @version 0.2.0
+ * @version 0.3.0
*/
export function GetStatusString(status: EventStatus): string {
switch (status) {
@@ -142,5 +149,9 @@ export function GetStatusString(status: EventStatus): string {
return StatusEnum.Cancelled;
case EventStatus.Active:
return StatusEnum.Active;
+ case EventStatus.PendingReview:
+ return StatusEnum.PendingReview;
+ case EventStatus.Reviewed:
+ return StatusEnum.Reviewed;
}
}
diff --git a/src/Components/Event/EventApprove.tsx b/src/Components/Event/EventApprove.tsx
new file mode 100644
index 0000000..d961932
--- /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.2
+ */
+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}`,
+ 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}`,
+ Status: EventStatus.Reviewed,
+ });
+ Update();
+ }, {
+ manual: true,
+ });
+
+ return (
+ runAsync()}
+ >
+
+ Approve
+
+ );
+}
diff --git a/src/Components/Event/EventCard.tsx b/src/Components/Event/EventCard.tsx
index 58aac46..b8ee33f 100644
--- a/src/Components/Event/EventCard.tsx
+++ b/src/Components/Event/EventCard.tsx
@@ -5,6 +5,7 @@ import { Authorized } 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";
@@ -23,7 +24,7 @@ import { EventExtract } from "./EventExtract";
*
* @author Aloento
* @since 1.0.0
- * @version 0.2.0
+ * @version 0.3.0
*/
export function EventCard({ Event }: { Event: Models.IEvent }) {
return (
@@ -39,10 +40,18 @@ export function EventCard({ Event }: { Event: Models.IEvent }) {
+ {
+ return Event.Status === EventStatus.PendingReview &&
+ groups.some(g => g === "/sd_operators" || g === "/sd_admins");
+ }}>
+
+
+
{
Event.RegionServices.size > 1 &&
}
+
@@ -72,6 +81,13 @@ export function EventCard({ Event }: { Event: Models.IEvent }) {
}
+
+
+ {Event.ContactEmail &&
+ }
+
@@ -95,6 +111,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..72ffd8f 100644
--- a/src/Components/Event/EventEditor.tsx
+++ b/src/Components/Event/EventEditor.tsx
@@ -1,6 +1,7 @@
import { ScaleButton, ScaleDropdownSelect, ScaleDropdownSelectItem, ScaleIconActionEdit, ScaleModal, ScaleTextarea, ScaleTextField } from "@telekom/scale-components-react";
import { useBoolean } from "ahooks";
import dayjs from "dayjs";
+import { useAuth } from "react-oidc-context";
import { Dic } from "~/Helpers/Entities";
import { Models } from "~/Services/Status.Models";
import { EventStatus, EventType, GetStatusList, IsIncident, IsOpenStatus } from "./Enums";
@@ -20,16 +21,17 @@ 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);
const [open, { setTrue, setFalse }] = useBoolean();
+ const auth = useAuth();
return <>
- Edit
+ Edit
- {GetStatusList(State.type)
+ {GetStatusList(State.type, Event.Status, (auth.user?.profile as any)?.groups)
.map((status, i) =>
{status}
@@ -125,6 +127,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}
+ />
+ )}
+
- Extract
+ Extract
();
+ 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) {
@@ -185,10 +212,11 @@ export function useEditForm(event: Models.IEvent) {
}, [start, end]);
const getToken = useAccessToken();
+ const { dispatchToast } = useToastController();
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 +230,11 @@ export function useEditForm(event: Models.IEvent) {
description,
};
+ if (type === EventType.Maintenance && contactEmail) {
+ body.contact_email = contactEmail;
+ body.version = event.Version ?? event.Histories.size + 1;
+ };
+
if (event.Type !== type) {
body.status = StatusEnum.ImpactChanged;
}
@@ -234,7 +267,15 @@ export function useEditForm(event: Models.IEvent) {
});
if (!raw.ok) {
- throw new Error("Failed to update event: " + await raw.text());
+ const message = await raw.text();
+ dispatchToast(
+
+ Failed to update event
+ {message}
+ ,
+ { intent: "warning" }
+ );
+ throw new Error("Failed to update event: " + message);
}
const eventIndex = DB.Events.findIndex(e => e.Id === event.Id);
@@ -246,6 +287,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 +315,7 @@ export function useEditForm(event: Models.IEvent) {
type,
update,
description,
+ contactEmail,
status,
start,
end,
@@ -283,6 +326,7 @@ export function useEditForm(event: Models.IEvent) {
setType,
setUpdate,
setDescription,
+ setContactEmail,
setStatus,
setStart,
setEnd,
@@ -293,6 +337,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/Event/useEventExtract.ts b/src/Components/Event/useEventExtract.tsx
similarity index 78%
rename from src/Components/Event/useEventExtract.ts
rename to src/Components/Event/useEventExtract.tsx
index 3c682c4..e4196f1 100644
--- a/src/Components/Event/useEventExtract.ts
+++ b/src/Components/Event/useEventExtract.tsx
@@ -1,3 +1,4 @@
+import { Toast, ToastBody, ToastTitle, useToastController } from "@fluentui/react-components";
import { useRequest } from "ahooks";
import { useState } from "react";
import { useStatus } from "~/Services/Status";
@@ -8,7 +9,7 @@ import { useRouter } from "../Router";
/**
* @author Aloento
* @since 1.0.0
- * @version 0.1.0
+ * @version 0.1.1
*/
export function useEventExtract(event: Models.IEvent) {
const [services, _setServices] = useState([]);
@@ -29,6 +30,7 @@ export function useEventExtract(event: Models.IEvent) {
}
const getToken = useAccessToken();
+ const { dispatchToast } = useToastController();
const { Nav } = useRouter();
const { Refresh } = useStatus();
@@ -52,6 +54,18 @@ export function useEventExtract(event: Models.IEvent) {
body: JSON.stringify(body)
});
+ if (!raw.ok) {
+ const message = await raw.text();
+ dispatchToast(
+
+ Failed to extract event
+ {message}
+ ,
+ { intent: "warning" }
+ );
+ throw new Error("Failed to extract event: " + message);
+ }
+
const res = await raw.json();
const id = res.id;
diff --git a/src/Components/Home/ServiceItem.css b/src/Components/Home/ServiceItem.css
deleted file mode 100644
index 2c3a695..0000000
--- 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 7e17fd0..9904cf3 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 8dde1fa..2a42228 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 { EventStatus } from "~/Components/Event/Enums";
+import { useStatus } from "~/Services/Status";
import { Authorized } 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,6 +48,16 @@ export function MobileMenu() {
New Event
+ {pendingCount > 0 && (
+
+ Reviews: {pendingCount}
+
+ )}
+
+
+ You're {((auth.user?.profile as any)?.groups as string[])?.filter(x => x.includes("sd"))}
+
+
auth.signoutSilent()}>
Logout {auth.user?.profile.name}
diff --git a/src/Components/Layout/NavItem.tsx b/src/Components/Layout/NavItem.tsx
index 3dcb457..5075501 100644
--- a/src/Components/Layout/NavItem.tsx
+++ b/src/Components/Layout/NavItem.tsx
@@ -16,7 +16,7 @@ export function NavItem({ Href, Label }: INavItem) {
const path = Paths.at(0);
return (
-
+
{Label}
);
diff --git a/src/Components/Layout/ProfileMenu.tsx b/src/Components/Layout/ProfileMenu.tsx
index 2d5bbae..42ca07a 100644
--- a/src/Components/Layout/ProfileMenu.tsx
+++ b/src/Components/Layout/ProfileMenu.tsx
@@ -4,7 +4,7 @@ import { useAuth } from "react-oidc-context";
/**
* @author Aloento
* @since 1.0.0
- * @version 0.1.0
+ * @version 0.2.0
*/
export function ProfileMenu() {
const auth = useAuth();
@@ -13,9 +13,11 @@ export function ProfileMenu() {
-
-
-
+
+
+
+
+
@@ -23,9 +25,7 @@ export function ProfileMenu() {
-
- New Event
-
+ You're {((auth.user?.profile as any)?.groups as string[])?.filter(x => x.includes("sd"))}
auth.signoutSilent()}>
diff --git a/src/Components/Layout/TopNavBar.tsx b/src/Components/Layout/TopNavBar.tsx
index 2e4225d..0103b51 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 0767e5d..3f156cc 100644
--- a/src/Components/New/NewForm.tsx
+++ b/src/Components/New/NewForm.tsx
@@ -1,4 +1,4 @@
-import { ScaleButton, ScaleDropdownSelect, ScaleDropdownSelectItem, ScaleHelperText, ScaleTable, ScaleTextarea, ScaleTextField } from "@telekom/scale-components-react";
+import { ScaleButton, ScaleDropdownSelect, ScaleDropdownSelectItem, ScaleHelperText, ScaleModal, ScaleTable, ScaleTextarea, ScaleTextField } from "@telekom/scale-components-react";
import dayjs from "dayjs";
import { orderBy } from "lodash";
import { Dic } from "~/Helpers/Entities";
@@ -21,140 +21,194 @@ import { useNewForm } from "./useNewForm";
*
* @author Aloento
* @since 1.0.0
- * @version 0.1.0
+ * @version 0.3.0
*/
export function NewForm() {
const { DB } = useStatus();
const { State, Actions, Validation, OnSubmit, Loading } = useNewForm();
return (
-
+
+ e.preventDefault()}
>
- Submit
-
-
+
+
+ Maintenance start time is earlier than the recommended 36 hours.
+
+
+ Selected: {dayjs(State.start).format("YYYY-MM-DD HH:mm")}
+
+
+ Recommended after: {dayjs().add(36, "hour").format("YYYY-MM-DD HH:mm")}
+
+
+
+
+ Cancel
+
+
+
+ Confirm Submit
+
+
+
+
+ >
)
}
diff --git a/src/Components/New/useNewForm.ts b/src/Components/New/useNewForm.ts
index 87c20e1..caf3c2c 100644
--- a/src/Components/New/useNewForm.ts
+++ b/src/Components/New/useNewForm.ts
@@ -15,7 +15,7 @@ import { useRouter } from "../Router";
*
* @author Aloento
* @since 1.0.0
- * @version 0.2.0
+ * @version 0.3.0
*/
export function useNewForm() {
const { DB, Update } = useStatus();
@@ -57,6 +57,8 @@ export function useNewForm() {
_setType(value);
setValType(undefined);
+ setIsShortConfirmed(false);
+ setStartNeedsConfirm(false);
if (IsIncident(value)) {
_setEnd(undefined);
@@ -84,7 +86,16 @@ export function useNewForm() {
const [start, _setStart] = useState(new Date());
const [valStart, setValStart] = useState();
- function setStart(value = start) {
+ const [isShortConfirmed, setIsShortConfirmed] = useState(false);
+ const [startNeedsConfirm, setStartNeedsConfirm] = useState(false);
+
+ function dismissStartConfirm() {
+ setStartNeedsConfirm(false);
+ setIsShortConfirmed(false);
+ }
+
+ function setStart(value = start, options: { resetConfirm?: boolean } = {}) {
+ const { resetConfirm = true } = options;
let err: boolean = false;
const now = new Date();
@@ -101,6 +112,10 @@ export function useNewForm() {
setValStart(undefined);
}
_setStart(value);
+ if (resetConfirm) {
+ setIsShortConfirmed(false);
+ setStartNeedsConfirm(false);
+ }
return !err;
}
@@ -126,7 +141,34 @@ export function useNewForm() {
useEffect(() => {
setStart();
setEnd();
- }, [start, end]);
+ setContactEmail();
+ }, [start, end, type]);
+
+ const [contactEmail, _setContactEmail] = useState("");
+ const [valContactEmail, setValContactEmail] = useState();
+ 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();
@@ -150,12 +192,25 @@ 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(start, { resetConfirm: false }), setEnd(), setServices(), setContactEmail()].every(Boolean)) {
return;
}
+ const now = new Date();
+ const minMaintenanceStart = new Date(now.getTime() + 36 * 60 * 60 * 1000);
+ const isShortMaintenanceStart = type === EventType.Maintenance && start < minMaintenanceStart;
+
+ if (isShortMaintenanceStart && !isShortConfirmed) {
+ setStartNeedsConfirm(true);
+ setIsShortConfirmed(true);
+ return;
+ }
+
+ setIsShortConfirmed(false);
+ setStartNeedsConfirm(false);
+
const status = IsIncident(type)
- ? EventStatus.Detected : EventStatus.Planned
+ ? EventStatus.Detected : EventStatus.PendingReview
const event: Models.IEvent = {
Id: Math.max(...DB.Events.map(event => event.Id), 0) + 1,
@@ -180,6 +235,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 +274,8 @@ export function useNewForm() {
description,
start,
end,
- services
+ services,
+ contactEmail
},
Actions: {
setTitle,
@@ -223,15 +283,19 @@ export function useNewForm() {
setDescription,
setStart,
setEnd,
- setServices
+ setServices,
+ setContactEmail,
+ dismissStartConfirm
},
Validation: {
title: valTitle,
type: valType,
description: valDescription,
start: valStart,
+ startNeedsConfirm,
end: valEnd,
- services: valServices
+ services: valServices,
+ contactEmail: valContactEmail
},
OnSubmit: runAsync,
Loading: loading
diff --git a/src/Helpers/Entities.ts b/src/Helpers/Entities.ts
index 8ef881e..6aeed7a 100644
--- a/src/Helpers/Entities.ts
+++ b/src/Helpers/Entities.ts
@@ -12,7 +12,7 @@ export const Dic = {
TZ: "Europe/Berlin",
Time: "DD MMM YY, HH:mm",
TimeTZ: "DD MMM YYYY, HH:mm [CET]",
- Picker: "YYYY-MM-DDTHH:mm:ss"
+ Picker: "YYYY-MM-DDTHH:mm"
};
/**
diff --git a/src/Pages/History.tsx b/src/Pages/History.tsx
index 3be48d4..cbc2e0d 100644
--- a/src/Pages/History.tsx
+++ b/src/Pages/History.tsx
@@ -3,6 +3,7 @@ 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 { EventFilters } from "~/Components/History/EventFilters";
import { getEventTag } from "~/Components/History/EventTag";
import { useEventFilters } from "~/Components/History/useEventFilters";
@@ -15,7 +16,7 @@ const PAGE_SIZE_OPTIONS = [10, 20, 50];
/**
* @author Aloento
* @since 1.2.0
- * @version 1.2.2
+ * @version 1.3.0
*/
export function History() {
const { DB } = useStatus();
@@ -26,6 +27,10 @@ export function History() {
return stored ? parseInt(stored, 10) : 20;
});
+ const events = DB.Events.filter(
+ (x) => x.Status !== EventStatus.PendingReview
+ );
+
const {
filters,
validation,
@@ -33,7 +38,7 @@ export function History() {
setFilters,
setValidation,
clearFilters,
- } = useEventFilters(DB.Events);
+ } = useEventFilters(events);
useEffect(() => {
if (!gridRef.current) {
@@ -102,7 +107,7 @@ export function History() {
filters={filters}
validation={validation}
regions={DB.Regions}
- totalEvents={DB.Events.length}
+ totalEvents={events.length}
filteredCount={filteredEvents.length}
onFiltersChange={setFilters}
onValidationChange={setValidation}
@@ -117,9 +122,9 @@ export function History() {
hideBorder
ref={gridRef}
>
- |