diff --git a/Common/Types/Date.ts b/Common/Types/Date.ts index dcd1da0c2e..5deabe9293 100644 --- a/Common/Types/Date.ts +++ b/Common/Types/Date.ts @@ -1,5 +1,5 @@ import PositiveNumber from './PositiveNumber'; -import moment from 'moment'; +import moment from 'moment-timezone'; import InBetween from './Database/InBetween'; export default class OneUptimeDate { @@ -316,12 +316,14 @@ export default class OneUptimeDate { date: string | Date, onlyShowDate?: boolean ): string { - let formatstring: string = 'MMM DD YYYY, HH:mm'; + + + let formatstring: string = 'MMM DD YYYY, HH:mm z'; if (onlyShowDate) { formatstring = 'MMM DD, YYYY'; } - + return moment(date).format(formatstring); } @@ -329,7 +331,7 @@ export default class OneUptimeDate { date: string | Date, onlyShowDate?: boolean ): string { - let formatstring: string = 'MMM DD YYYY, HH:mm'; + let formatstring: string = 'MMM DD YYYY, HH:mm z'; if (onlyShowDate) { formatstring = 'MMM DD, YYYY'; diff --git a/Common/package-lock.json b/Common/package-lock.json index 41b6e25c1b..1c3d0d3736 100644 --- a/Common/package-lock.json +++ b/Common/package-lock.json @@ -15,6 +15,7 @@ "axios": "^0.26.1", "crypto-js": "^4.1.1", "moment": "^2.29.2", + "moment-timezone": "^0.5.40", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", "posthog-js": "^1.37.0", @@ -3284,6 +3285,17 @@ "node": "*" } }, + "node_modules/moment-timezone": { + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", + "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", + "dependencies": { + "moment": ">= 2.9.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7062,6 +7074,14 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" }, + "moment-timezone": { + "version": "0.5.40", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz", + "integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==", + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/Common/package.json b/Common/package.json index ba3d12c74c..85fee1ba5f 100644 --- a/Common/package.json +++ b/Common/package.json @@ -23,6 +23,7 @@ "axios": "^0.26.1", "crypto-js": "^4.1.1", "moment": "^2.29.2", + "moment-timezone": "^0.5.40", "nanoid": "^3.3.2", "nanoid-dictionary": "^4.3.0", "posthog-js": "^1.37.0", diff --git a/StatusPage/src/Pages/Incidents/Detail.tsx b/StatusPage/src/Pages/Incidents/Detail.tsx index 097dba3b0f..50bde8ad9e 100644 --- a/StatusPage/src/Pages/Incidents/Detail.tsx +++ b/StatusPage/src/Pages/Incidents/Detail.tsx @@ -1,11 +1,208 @@ -import React, { FunctionComponent, ReactElement } from 'react'; +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from 'react'; import PageComponentProps from '../PageComponentProps'; import Page from '../../Components/Page/Page'; +import URL from 'Common/Types/API/URL'; +import PageLoader from 'CommonUI/src/Components/Loader/PageLoader'; +import BaseAPI from 'CommonUI/src/Utils/API/API'; +import { DASHBOARD_API_URL } from 'CommonUI/src/Config'; +import useAsyncEffect from 'use-async-effect'; +import { JSONArray, JSONObject } from 'Common/Types/JSON'; +import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse'; +import ErrorMessage from 'CommonUI/src/Components/ErrorMessage/ErrorMessage'; +import BadDataException from 'Common/Types/Exception/BadDataException'; +import LocalStorage from 'CommonUI/src/Utils/LocalStorage'; +import ObjectID from 'Common/Types/ObjectID'; +import BaseModel from 'Common/Models/BaseModel'; +import { ComponentProps as EventItemComponentProps } from 'CommonUI/src/Components/EventItem/EventItem'; +import StatusPageResource from 'Model/Models/StatusPageResource'; +import Incident from 'Model/Models/Incident'; +import IncidentPublicNote from 'Model/Models/IncidentPublicNote'; +import OneUptimeDate from 'Common/Types/Date'; +import IncidentStateTimeline from 'Model/Models/IncidentStateTimeline'; +import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; +import PageMap from '../../Utils/PageMap'; +import Route from 'Common/Types/API/Route'; +import HTTPResponse from 'Common/Types/API/HTTPResponse'; +import EventItem, { TimelineItem } from 'CommonUI/src/Components/EventItem/EventItem'; +import Navigation from 'CommonUI/src/Utils/Navigation'; -const Overview: FunctionComponent = ( - _props: PageComponentProps +const Detail: FunctionComponent = ( + props: PageComponentProps ): ReactElement => { - return {/* */}; + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [statusPageResources, setStatusPageResources] = useState< + Array + >([]); + const [incidentPublicNotes, setIncidentPublicNotes] = useState< + Array + >([]); + const [incident, setIncident] = useState(null); + const [incidentStateTimelines, setIncidentStateTimelines] = useState< + Array + >([]); + const [parsedData, setParsedData] = + useState(null); + + useAsyncEffect(async () => { + try { + setIsLoading(true); + + const id: ObjectID = LocalStorage.getItem( + 'statusPageId' + ) as ObjectID; + + const incidentId = Navigation.getLastParam()?.toString().replace("/", ""); + + if (!id) { + throw new BadDataException('Status Page ID is required'); + } + const response: HTTPResponse = + await BaseAPI.post( + URL.fromString(DASHBOARD_API_URL.toString()).addRoute( + `/status-page/incidents/${id.toString()}/${incidentId?.toString()}` + ), + {}, + {} + ); + const data: JSONObject = response.data; + + const incidentPublicNotes: Array = + BaseModel.fromJSONArray( + (data['incidentPublicNotes'] as JSONArray) || [], + IncidentPublicNote + ); + const incident: Incident = BaseModel.fromJSONObject( + (data['incident'] as JSONObject) || [], + Incident + ); + const statusPageResources: Array = + BaseModel.fromJSONArray( + (data['statusPageResources'] as JSONArray) || [], + StatusPageResource + ); + const incidentStateTimelines: Array = + BaseModel.fromJSONArray( + (data['incidentStateTimelines'] as JSONArray) || [], + IncidentStateTimeline + ); + + // save data. set() + setIncidentPublicNotes(incidentPublicNotes); + setIncident(incident); + setStatusPageResources(statusPageResources); + setIncidentStateTimelines(incidentStateTimelines); + + setIsLoading(false); + props.onLoadComplete(); + } catch (err) { + try { + setError( + (err as HTTPErrorResponse).message || + 'Server Error. Please try again' + ); + } catch (e) { + setError('Server Error. Please try again'); + } + setIsLoading(false); + } + }, []); + + useEffect(() => { + if (isLoading) { + // parse data; + setParsedData(null); + return; + } + + + if (!incident) { + return; + } + + + const timeline: Array = []; + + for (const incidentPublicNote of incidentPublicNotes) { + if ( + incidentPublicNote.incidentId?.toString() === + incident.id?.toString() + ) { + timeline.push({ + text: (Update - {incidentPublicNote?.note}), + date: incidentPublicNote?.createdAt!, + isBold: false, + }); + } + } + + for (const incidentStateTimeline of incidentStateTimelines) { + if ( + incidentStateTimeline.incidentId?.toString() === + incident.id?.toString() + ) { + timeline.push({ + text: incidentStateTimeline.incidentState?.name || '', + date: incidentStateTimeline?.createdAt!, + isBold: true, + }); + } + } + + timeline.sort((a: TimelineItem, b: TimelineItem) => { + return OneUptimeDate.isAfter(a.date, b.date) === true ? 1 : -1; + }); + + const monitorIds = incident.monitors?.map((monitor) => monitor._id) || []; + + const namesOfResources = statusPageResources.filter((resource) => monitorIds.includes(resource.monitorId?.toString())); + + const data = { + eventTitle: incident.title || '', + eventDescription: incident.description, + eventResourcesAffected: namesOfResources.map((i) => i.displayName || ''), + eventTimeline: timeline, + eventType: 'Incident', + eventViewRoute: RouteUtil.populateRouteParams( + props.isPreviewPage + ? (RouteMap[PageMap.PREVIEW_INCIDENT_DETAIL] as Route) + : (RouteMap[PageMap.INCIDENT_DETAIL] as Route), + incident.id! + ), + }; + + + setParsedData(data); + }, [isLoading]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!parsedData) { + return ; + } + + return ( + + + {incident ? : <>} + {!incident ? ( + + ) : <>} + + ); }; -export default Overview; +export default Detail; \ No newline at end of file diff --git a/StatusPage/src/Pages/Incidents/List.tsx b/StatusPage/src/Pages/Incidents/List.tsx index 71c6718b3f..57b93139d7 100644 --- a/StatusPage/src/Pages/Incidents/List.tsx +++ b/StatusPage/src/Pages/Incidents/List.tsx @@ -39,7 +39,7 @@ const Overview: FunctionComponent = ( ): ReactElement => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [_statusPageResources, setStatusPageResources] = useState< + const [statusPageResources, setStatusPageResources] = useState< Array >([]); const [incidentPublicNotes, setIncidentPublicNotes] = useState< @@ -148,7 +148,7 @@ const Overview: FunctionComponent = ( incident.id?.toString() ) { timeline.push({ - text: incidentPublicNote?.note || '', + text: (Update - {incidentPublicNote?.note}), date: incidentPublicNote?.createdAt!, isBold: false, }); @@ -172,9 +172,14 @@ const Overview: FunctionComponent = ( return OneUptimeDate.isAfter(a.date, b.date) === true ? 1 : -1; }); + const monitorIds = incident.monitors?.map((monitor) => monitor._id) || []; + + const namesOfResources = statusPageResources.filter((resource) => monitorIds.includes(resource.monitorId?.toString())); + days[dayString]?.items.push({ eventTitle: incident.title || '', eventDescription: incident.description, + eventResourcesAffected: namesOfResources.map((i)=>i.displayName || ''), eventTimeline: timeline, eventType: 'Incident', eventViewRoute: RouteUtil.populateRouteParams(