@oneuptime/common 7.0.4922 → 7.0.4976

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/Models/DatabaseModels/CallLog.ts +578 -0
  2. package/Models/DatabaseModels/EmailLog.ts +579 -0
  3. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -1
  4. package/Models/DatabaseModels/Index.ts +4 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicy.ts +1 -1
  6. package/Models/DatabaseModels/PushNotificationLog.ts +877 -0
  7. package/Models/DatabaseModels/SmsLog.ts +578 -0
  8. package/Models/DatabaseModels/WorkspaceNotificationLog.ts +931 -0
  9. package/Server/API/StatusPageAPI.ts +2 -0
  10. package/Server/API/UserPushAPI.ts +12 -1
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1754776130988-MigrationName.ts +259 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1754828812691-MigrationName.ts +105 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1754910440587-MigrationName.ts +105 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1755030730926-MigrationName.ts +101 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1755088852971-MigrationName.ts +371 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1755093133870-MigrationName.ts +29 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1755109893911-MigrationName.ts +23 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1755110936888-MigrationName.ts +41 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  20. package/Server/Services/CallService.ts +26 -1
  21. package/Server/Services/Index.ts +2 -0
  22. package/Server/Services/MailService.ts +60 -0
  23. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +6 -0
  24. package/Server/Services/OnCallDutyPolicyScheduleService.ts +12 -0
  25. package/Server/Services/ProjectService.ts +3 -7
  26. package/Server/Services/PushNotificationLogService.ts +14 -0
  27. package/Server/Services/PushNotificationService.ts +129 -13
  28. package/Server/Services/ScheduledMaintenanceService.ts +4 -0
  29. package/Server/Services/SmsService.ts +25 -0
  30. package/Server/Services/StatusPagePrivateUserService.ts +1 -0
  31. package/Server/Services/StatusPageService.ts +1 -0
  32. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  33. package/Server/Services/TeamMemberService.ts +1 -0
  34. package/Server/Services/UserCallService.ts +1 -0
  35. package/Server/Services/UserEmailService.ts +1 -0
  36. package/Server/Services/UserNotificationRuleService.ts +85 -6
  37. package/Server/Services/UserNotificationSettingService.ts +58 -0
  38. package/Server/Services/UserOnCallLogService.ts +1 -0
  39. package/Server/Services/UserSmsService.ts +1 -0
  40. package/Server/Services/WorkspaceNotificationLogService.ts +276 -0
  41. package/Server/Services/WorkspaceNotificationRuleService.ts +290 -33
  42. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +21 -6
  43. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +66 -0
  44. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +66 -1
  45. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +65 -0
  46. package/Server/Utils/Workspace/Slack/Slack.ts +21 -6
  47. package/Types/Permission.ts +20 -0
  48. package/Types/PushNotification/PushNotificationRequest.ts +4 -1
  49. package/Types/PushNotification/PushStatus.ts +6 -0
  50. package/Types/Workspace/WorkspaceNotificationActionType.ts +8 -0
  51. package/Types/Workspace/WorkspaceNotificationStatus.ts +6 -0
  52. package/Typings/elkjs.d.ts +30 -0
  53. package/UI/Components/Detail/Detail.tsx +1 -1
  54. package/UI/Components/Graphs/ServiceDependencyGraph.tsx +281 -0
  55. package/UI/Components/Tabs/Tab.tsx +8 -9
  56. package/UI/Components/Tabs/Tabs.tsx +17 -16
  57. package/Utils/Uptime/UptimeUtil.ts +20 -1
  58. package/build/dist/Models/DatabaseModels/CallLog.js +580 -0
  59. package/build/dist/Models/DatabaseModels/CallLog.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/EmailLog.js +580 -0
  61. package/build/dist/Models/DatabaseModels/EmailLog.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -1
  63. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  65. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js +1 -1
  67. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/PushNotificationLog.js +904 -0
  69. package/build/dist/Models/DatabaseModels/PushNotificationLog.js.map +1 -0
  70. package/build/dist/Models/DatabaseModels/SmsLog.js +580 -0
  71. package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js +961 -0
  73. package/build/dist/Models/DatabaseModels/WorkspaceNotificationLog.js.map +1 -0
  74. package/build/dist/Server/API/StatusPageAPI.js +2 -0
  75. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  76. package/build/dist/Server/API/UserPushAPI.js +9 -1
  77. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754776130988-MigrationName.js +104 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754776130988-MigrationName.js.map +1 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754828812691-MigrationName.js +42 -0
  81. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754828812691-MigrationName.js.map +1 -0
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754910440587-MigrationName.js +42 -0
  83. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1754910440587-MigrationName.js.map +1 -0
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755030730926-MigrationName.js +44 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755030730926-MigrationName.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755088852971-MigrationName.js +134 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755088852971-MigrationName.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755093133870-MigrationName.js +16 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755093133870-MigrationName.js.map +1 -0
  90. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755109893911-MigrationName.js +14 -0
  91. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755109893911-MigrationName.js.map +1 -0
  92. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755110936888-MigrationName.js +20 -0
  93. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1755110936888-MigrationName.js.map +1 -0
  94. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  96. package/build/dist/Server/Services/CallService.js +12 -1
  97. package/build/dist/Server/Services/CallService.js.map +1 -1
  98. package/build/dist/Server/Services/Index.js +2 -0
  99. package/build/dist/Server/Services/Index.js.map +1 -1
  100. package/build/dist/Server/Services/MailService.js +37 -0
  101. package/build/dist/Server/Services/MailService.js.map +1 -1
  102. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +4 -0
  103. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  104. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +9 -0
  105. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  106. package/build/dist/Server/Services/ProjectService.js +3 -5
  107. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  108. package/build/dist/Server/Services/PushNotificationLogService.js +13 -0
  109. package/build/dist/Server/Services/PushNotificationLogService.js.map +1 -0
  110. package/build/dist/Server/Services/PushNotificationService.js +98 -13
  111. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  112. package/build/dist/Server/Services/ScheduledMaintenanceService.js +4 -0
  113. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  114. package/build/dist/Server/Services/SmsService.js +12 -1
  115. package/build/dist/Server/Services/SmsService.js.map +1 -1
  116. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
  117. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  118. package/build/dist/Server/Services/StatusPageService.js +1 -0
  119. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  120. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  121. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  122. package/build/dist/Server/Services/TeamMemberService.js +1 -0
  123. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  124. package/build/dist/Server/Services/UserCallService.js +1 -0
  125. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +1 -0
  127. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  128. package/build/dist/Server/Services/UserNotificationRuleService.js +70 -6
  129. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  130. package/build/dist/Server/Services/UserNotificationSettingService.js +43 -0
  131. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  132. package/build/dist/Server/Services/UserOnCallLogService.js +1 -0
  133. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  134. package/build/dist/Server/Services/UserSmsService.js +1 -0
  135. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  136. package/build/dist/Server/Services/WorkspaceNotificationLogService.js +181 -0
  137. package/build/dist/Server/Services/WorkspaceNotificationLogService.js.map +1 -0
  138. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +193 -3
  139. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  140. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +19 -6
  141. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  142. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +48 -0
  143. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  144. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +48 -1
  145. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  146. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +47 -0
  147. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  148. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +19 -6
  149. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  150. package/build/dist/Types/Permission.js +16 -0
  151. package/build/dist/Types/Permission.js.map +1 -1
  152. package/build/dist/Types/PushNotification/PushStatus.js +7 -0
  153. package/build/dist/Types/PushNotification/PushStatus.js.map +1 -0
  154. package/build/dist/Types/Workspace/WorkspaceNotificationActionType.js +9 -0
  155. package/build/dist/Types/Workspace/WorkspaceNotificationActionType.js.map +1 -0
  156. package/build/dist/Types/Workspace/WorkspaceNotificationStatus.js +7 -0
  157. package/build/dist/Types/Workspace/WorkspaceNotificationStatus.js.map +1 -0
  158. package/build/dist/UI/Components/Detail/Detail.js +1 -1
  159. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  160. package/build/dist/UI/Components/Graphs/ServiceDependencyGraph.js +206 -0
  161. package/build/dist/UI/Components/Graphs/ServiceDependencyGraph.js.map +1 -0
  162. package/build/dist/UI/Components/Tabs/Tab.js +6 -5
  163. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  164. package/build/dist/UI/Components/Tabs/Tabs.js +5 -6
  165. package/build/dist/UI/Components/Tabs/Tabs.js.map +1 -1
  166. package/build/dist/Utils/Uptime/UptimeUtil.js +10 -1
  167. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  168. package/package.json +2 -1
@@ -34,6 +34,7 @@ import OneUptimeDate from "../../../../../Types/Date";
34
34
  import AccessTokenService from "../../../../Services/AccessTokenService";
35
35
  import CaptureSpan from "../../../Telemetry/CaptureSpan";
36
36
  import WorkspaceType from "../../../../../Types/Workspace/WorkspaceType";
37
+ import WorkspaceNotificationLogService from "../../../../Services/WorkspaceNotificationLogService";
37
38
 
38
39
  export default class SlackScheduledMaintenanceActions {
39
40
  @CaptureSpan()
@@ -582,6 +583,38 @@ export default class SlackScheduledMaintenanceActions {
582
583
  userId,
583
584
  );
584
585
 
586
+ // Log the button interaction
587
+ if (slackRequest.projectId) {
588
+ try {
589
+ const logData: {
590
+ projectId: ObjectID;
591
+ workspaceType: WorkspaceType;
592
+ channelId?: string;
593
+ userId: ObjectID;
594
+ buttonAction: string;
595
+ scheduledMaintenanceId?: ObjectID;
596
+ } = {
597
+ projectId: slackRequest.projectId,
598
+ workspaceType: WorkspaceType.Slack,
599
+ userId: userId,
600
+ buttonAction: "mark_scheduled_maintenance_as_ongoing",
601
+ };
602
+
603
+ if (slackRequest.slackChannelId) {
604
+ logData.channelId = slackRequest.slackChannelId;
605
+ }
606
+ logData.scheduledMaintenanceId = scheduledMaintenanceId;
607
+
608
+ await WorkspaceNotificationLogService.logButtonPressed(logData, {
609
+ isRoot: true,
610
+ });
611
+ } catch (err) {
612
+ logger.error("Error logging button interaction:");
613
+ logger.error(err);
614
+ // Don't throw the error, just log it so the main flow continues
615
+ }
616
+ }
617
+
585
618
  // Scheduled Maintenance Feed will send a message to the channel that the scheduledMaintenance has been Ongoing.
586
619
  return;
587
620
  }
@@ -679,6 +712,38 @@ export default class SlackScheduledMaintenanceActions {
679
712
  userId,
680
713
  );
681
714
 
715
+ // Log the button interaction
716
+ if (slackRequest.projectId) {
717
+ try {
718
+ const logData: {
719
+ projectId: ObjectID;
720
+ workspaceType: WorkspaceType;
721
+ channelId?: string;
722
+ userId: ObjectID;
723
+ buttonAction: string;
724
+ scheduledMaintenanceId?: ObjectID;
725
+ } = {
726
+ projectId: slackRequest.projectId,
727
+ workspaceType: WorkspaceType.Slack,
728
+ userId: userId,
729
+ buttonAction: "mark_scheduled_maintenance_as_complete",
730
+ };
731
+
732
+ if (slackRequest.slackChannelId) {
733
+ logData.channelId = slackRequest.slackChannelId;
734
+ }
735
+ logData.scheduledMaintenanceId = scheduledMaintenanceId;
736
+
737
+ await WorkspaceNotificationLogService.logButtonPressed(logData, {
738
+ isRoot: true,
739
+ });
740
+ } catch (err) {
741
+ logger.error("Error logging button interaction:");
742
+ logger.error(err);
743
+ // Don't throw the error, just log it so the main flow continues
744
+ }
745
+ }
746
+
682
747
  return;
683
748
  }
684
749
 
@@ -729,13 +729,28 @@ export default class SlackUtil extends WorkspaceBase {
729
729
 
730
730
  // add channel ids.
731
731
  for (const channelId of data.workspaceMessagePayload.channelIds) {
732
- const channel: WorkspaceChannel = {
733
- id: channelId,
734
- name: "",
735
- workspaceType: WorkspaceType.Slack,
736
- };
732
+ try {
733
+ // Get the channel info including name from channel ID
734
+ const channel: WorkspaceChannel =
735
+ await this.getWorkspaceChannelFromChannelId({
736
+ authToken: data.authToken,
737
+ channelId: channelId,
738
+ });
737
739
 
738
- workspaceChannelsToPostTo.push(channel);
740
+ workspaceChannelsToPostTo.push(channel);
741
+ } catch (err) {
742
+ logger.error(`Error getting channel info for channel ID ${channelId}:`);
743
+ logger.error(err);
744
+
745
+ // Fallback: create channel object with empty name if API call fails
746
+ const channel: WorkspaceChannel = {
747
+ id: channelId,
748
+ name: channelId,
749
+ workspaceType: WorkspaceType.Slack,
750
+ };
751
+
752
+ workspaceChannelsToPostTo.push(channel);
753
+ }
739
754
  }
740
755
 
741
756
  logger.debug("Channel IDs to post to:");
@@ -142,6 +142,8 @@ enum Permission {
142
142
  ReadSmsLog = "ReadSmsLog",
143
143
  ReadEmailLog = "ReadEmailLog",
144
144
  ReadCallLog = "ReadCallLog",
145
+ ReadPushLog = "ReadPushLog",
146
+ ReadWorkspaceNotificationLog = "ReadWorkspaceNotificationLog",
145
147
 
146
148
  CreateIncidentOwnerTeam = "CreateIncidentOwnerTeam",
147
149
  DeleteIncidentOwnerTeam = "DeleteIncidentOwnerTeam",
@@ -3003,6 +3005,24 @@ export class PermissionHelper {
3003
3005
  isAccessControlPermission: false,
3004
3006
  },
3005
3007
 
3008
+ {
3009
+ permission: Permission.ReadPushLog,
3010
+ title: "Read Push Log",
3011
+ description:
3012
+ "This permission can read Push Notification Logs of this project.",
3013
+ isAssignableToTenant: true,
3014
+ isAccessControlPermission: false,
3015
+ },
3016
+
3017
+ {
3018
+ permission: Permission.ReadWorkspaceNotificationLog,
3019
+ title: "Read Workspace Notification Log",
3020
+ description:
3021
+ "This permission can read Workspace Notification Logs (Slack / Microsoft Teams) of this project.",
3022
+ isAssignableToTenant: true,
3023
+ isAccessControlPermission: false,
3024
+ },
3025
+
3006
3026
  {
3007
3027
  permission: Permission.CreateMonitorProbe,
3008
3028
  title: "Create Monitor Probe",
@@ -1,5 +1,8 @@
1
1
  interface PushNotificationRequest {
2
- deviceTokens: string[];
2
+ devices: Array<{
3
+ token: string;
4
+ name?: string;
5
+ }>;
3
6
  message: {
4
7
  title: string;
5
8
  body: string;
@@ -0,0 +1,6 @@
1
+ enum PushStatus {
2
+ Success = "Success",
3
+ Error = "Error",
4
+ }
5
+
6
+ export default PushStatus;
@@ -0,0 +1,8 @@
1
+ enum WorkspaceNotificationActionType {
2
+ SendMessage = "SendMessage",
3
+ CreateChannel = "CreateChannel",
4
+ InviteUser = "InviteUser",
5
+ ButtonPressed = "ButtonPressed",
6
+ }
7
+
8
+ export default WorkspaceNotificationActionType;
@@ -0,0 +1,6 @@
1
+ enum WorkspaceNotificationStatus {
2
+ Success = "Success",
3
+ Error = "Error",
4
+ }
5
+
6
+ export default WorkspaceNotificationStatus;
@@ -0,0 +1,30 @@
1
+ declare module "elkjs/lib/elk.bundled.js" {
2
+ export interface ElkNode {
3
+ id?: string;
4
+ x?: number;
5
+ y?: number;
6
+ width?: number | undefined;
7
+ height?: number | undefined;
8
+ layoutOptions?: Record<string, string>;
9
+ children?: ElkNode[];
10
+ edges?: Array<ElkPrimitiveEdge | ElkExtendedEdge>;
11
+ }
12
+
13
+ export interface ElkPrimitiveEdge {
14
+ id: string;
15
+ sources: string[];
16
+ targets: string[];
17
+ }
18
+
19
+ export interface ElkExtendedEdge extends ElkPrimitiveEdge {
20
+ sections?: Array<{
21
+ startPoint?: { x: number; y: number };
22
+ endPoint?: { x: number; y: number };
23
+ bendPoints?: Array<{ x: number; y: number }>;
24
+ }>;
25
+ }
26
+
27
+ export default class ELK {
28
+ public layout(graph: ElkNode): Promise<ElkNode>;
29
+ }
30
+ }
@@ -420,7 +420,7 @@ const Detail: DetailFunction = <T extends GenericObject>(
420
420
  )}
421
421
  </div>
422
422
  )}
423
- {!data && field.placeholder && (
423
+ {(data === null || data === undefined) && field.placeholder && (
424
424
  <PlaceholderText text={field.placeholder} />
425
425
  )}
426
426
  </div>
@@ -0,0 +1,281 @@
1
+ import React, {
2
+ FunctionComponent,
3
+ ReactElement,
4
+ useEffect,
5
+ useState,
6
+ } from "react";
7
+ import ReactFlow, {
8
+ Background,
9
+ Controls,
10
+ Edge,
11
+ MarkerType,
12
+ MiniMap,
13
+ Node,
14
+ Position,
15
+ } from "reactflow";
16
+ import "reactflow/dist/style.css";
17
+ import type { ElkExtendedEdge, ElkNode } from "elkjs";
18
+ import ELK from "elkjs/lib/elk.bundled.js";
19
+
20
+ export interface ServiceNodeData {
21
+ id: string;
22
+ name: string;
23
+ color?: string;
24
+ }
25
+
26
+ export interface ServiceEdgeData {
27
+ fromServiceId: string;
28
+ toServiceId: string;
29
+ }
30
+
31
+ export interface ServiceDependencyGraphProps {
32
+ services: Array<ServiceNodeData>;
33
+ dependencies: Array<ServiceEdgeData>;
34
+ }
35
+
36
+ const ServiceDependencyGraph: FunctionComponent<ServiceDependencyGraphProps> = (
37
+ props: ServiceDependencyGraphProps,
38
+ ): ReactElement => {
39
+ const computeLuminance: (r: number, g: number, b: number) => number = (
40
+ r: number,
41
+ g: number,
42
+ b: number,
43
+ ): number => {
44
+ const transform: (v: number) => number = (v: number): number => {
45
+ const c: number = v / 255;
46
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
47
+ };
48
+ const R: number = transform(r);
49
+ const G: number = transform(g);
50
+ const B: number = transform(b);
51
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
52
+ };
53
+
54
+ const getContrastText: (bg?: string) => string = (bg?: string): string => {
55
+ if (!bg) {
56
+ return "#111827"; // gray-900
57
+ }
58
+ // normalize to hex like #rrggbb
59
+ let hex: string = bg.trim();
60
+ if (hex.startsWith("rgb")) {
61
+ // basic rgb(a) parser
62
+ const m: RegExpMatchArray | null = hex
63
+ .replace(/\s+/g, "")
64
+ .match(/rgba?\((\d+),(\d+),(\d+)/i);
65
+ if (m) {
66
+ const r: number = parseInt(m[1] as string, 10);
67
+ const g: number = parseInt(m[2] as string, 10);
68
+ const b: number = parseInt(m[3] as string, 10);
69
+ const luminance: number = computeLuminance(r, g, b);
70
+ return luminance > 0.5 ? "#111827" : "#ffffff";
71
+ }
72
+ return "#111827";
73
+ }
74
+ if (hex[0] === "#") {
75
+ hex = hex.slice(1);
76
+ }
77
+ if (hex.length === 3) {
78
+ hex = hex
79
+ .split("")
80
+ .map((c: string): string => {
81
+ return c + c;
82
+ })
83
+ .join("");
84
+ }
85
+ if (hex.length !== 6) {
86
+ return "#111827";
87
+ }
88
+ const r: number = parseInt(hex.slice(0, 2), 16);
89
+ const g: number = parseInt(hex.slice(2, 4), 16);
90
+ const b: number = parseInt(hex.slice(4, 6), 16);
91
+ const luminance: number = computeLuminance(r, g, b);
92
+ return luminance > 0.5 ? "#111827" : "#ffffff";
93
+ };
94
+
95
+ const [rfNodes, setRfNodes] = useState<Node[]>([]);
96
+ const [rfEdges, setRfEdges] = useState<Edge[]>([]);
97
+
98
+ useEffect((): void => {
99
+ const elk: any = new ELK();
100
+ // fixed node dimensions for layout (px)
101
+ const NODE_WIDTH: number = 220;
102
+ const NODE_HEIGHT: number = 56;
103
+
104
+ const sortedServices: Array<ServiceNodeData> = [...props.services].sort(
105
+ (a: ServiceNodeData, b: ServiceNodeData): number => {
106
+ return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
107
+ },
108
+ );
109
+ const sortedDeps: Array<ServiceEdgeData> = [...props.dependencies].sort(
110
+ (a: ServiceEdgeData, b: ServiceEdgeData): number => {
111
+ if (a.fromServiceId === b.fromServiceId) {
112
+ return a.toServiceId.localeCompare(b.toServiceId);
113
+ }
114
+ return a.fromServiceId.localeCompare(b.fromServiceId);
115
+ },
116
+ );
117
+
118
+ const elkGraph: ElkNode = {
119
+ id: "root",
120
+ layoutOptions: {
121
+ algorithm: "layered",
122
+ "elk.direction": "RIGHT",
123
+ "elk.layered.spacing.nodeNodeBetweenLayers": "120",
124
+ "elk.spacing.nodeNode": "60",
125
+ "elk.edgeRouting": "POLYLINE",
126
+ },
127
+ children: sortedServices.map((svc: ServiceNodeData): ElkNode => {
128
+ return {
129
+ id: svc.id,
130
+ width: NODE_WIDTH,
131
+ height: NODE_HEIGHT,
132
+ } as ElkNode;
133
+ }),
134
+ edges: sortedDeps.map((dep: ServiceEdgeData): ElkExtendedEdge => {
135
+ return {
136
+ id: `e-${dep.fromServiceId}-${dep.toServiceId}`,
137
+ sources: [dep.fromServiceId],
138
+ targets: [dep.toServiceId],
139
+ };
140
+ }),
141
+ };
142
+
143
+ const layout: () => Promise<void> = async (): Promise<void> => {
144
+ try {
145
+ const res: any = await elk.layout(elkGraph as any);
146
+ const placedNodes: Node[] = (res.children || []).map(
147
+ (child: any): Node => {
148
+ const svc: ServiceNodeData | undefined = sortedServices.find(
149
+ (s: ServiceNodeData): boolean => {
150
+ return s.id === child.id;
151
+ },
152
+ );
153
+ const background: string = svc?.color || "#ffffff";
154
+ const textColor: string = getContrastText(background);
155
+ return {
156
+ id: child.id || "",
157
+ data: { label: svc?.name || "" },
158
+ position: { x: child.x || 0, y: child.y || 0 },
159
+ sourcePosition: Position.Right,
160
+ targetPosition: Position.Left,
161
+ style: {
162
+ borderRadius: 8,
163
+ padding: 8,
164
+ border: "1px solid rgba(0,0,0,0.08)",
165
+ background,
166
+ color: textColor,
167
+ boxShadow: "0 1px 2px rgba(16,24,40,.05)",
168
+ width: NODE_WIDTH,
169
+ height: NODE_HEIGHT,
170
+ },
171
+ } as Node;
172
+ },
173
+ );
174
+
175
+ const stroke: string = "#94a3b8"; // slate-400
176
+ const placedEdges: Edge[] = sortedDeps.map(
177
+ (dep: ServiceEdgeData): Edge => {
178
+ return {
179
+ id: `e-${dep.fromServiceId}-${dep.toServiceId}`,
180
+ source: dep.fromServiceId,
181
+ target: dep.toServiceId,
182
+ animated: false,
183
+ style: { stroke, strokeWidth: 2 },
184
+ markerEnd: { type: MarkerType.Arrow, color: stroke },
185
+ type: "smoothstep",
186
+ };
187
+ },
188
+ );
189
+
190
+ setRfNodes(placedNodes);
191
+ setRfEdges(placedEdges);
192
+ } catch {
193
+ // Fallback: deterministic grid by name
194
+ const sorted: Array<ServiceNodeData> = sortedServices;
195
+ const COLS: number = 4;
196
+ const GAP_X: number = 260;
197
+ const GAP_Y: number = 120;
198
+ const nodes: Node[] = sorted.map(
199
+ (svc: ServiceNodeData, i: number): Node => {
200
+ const col: number = i % COLS;
201
+ const row: number = Math.floor(i / COLS);
202
+ const x: number = col * GAP_X;
203
+ const y: number = row * GAP_Y;
204
+ const background: string = svc.color || "#ffffff";
205
+ const textColor: string = getContrastText(background);
206
+ return {
207
+ id: svc.id,
208
+ data: { label: svc.name },
209
+ position: { x, y },
210
+ sourcePosition: Position.Right,
211
+ targetPosition: Position.Left,
212
+ style: {
213
+ borderRadius: 8,
214
+ padding: 8,
215
+ border: "1px solid rgba(0,0,0,0.08)",
216
+ background,
217
+ color: textColor,
218
+ boxShadow: "0 1px 2px rgba(16,24,40,.05)",
219
+ width: NODE_WIDTH,
220
+ height: NODE_HEIGHT,
221
+ },
222
+ };
223
+ },
224
+ );
225
+ const stroke: string = "#94a3b8";
226
+ const edges: Edge[] = sortedDeps.map((dep: ServiceEdgeData): Edge => {
227
+ return {
228
+ id: `e-${dep.fromServiceId}-${dep.toServiceId}`,
229
+ source: dep.fromServiceId,
230
+ target: dep.toServiceId,
231
+ animated: false,
232
+ style: { stroke, strokeWidth: 2 },
233
+ markerEnd: { type: MarkerType.Arrow, color: stroke },
234
+ type: "smoothstep",
235
+ };
236
+ });
237
+ setRfNodes(nodes);
238
+ setRfEdges(edges);
239
+ }
240
+ };
241
+
242
+ layout();
243
+ }, [props.services, props.dependencies]);
244
+
245
+ return (
246
+ <div style={{ width: "100%", height: 600 }}>
247
+ <style>{`
248
+ /* Hide/transparentize connection handles (ports) for read-only view */
249
+ .service-dependency-graph .react-flow__handle {
250
+ background: transparent !important;
251
+ border-color: transparent !important;
252
+ }
253
+ `}</style>
254
+ <ReactFlow
255
+ className="service-dependency-graph"
256
+ nodes={rfNodes}
257
+ edges={rfEdges}
258
+ fitView
259
+ nodesDraggable={false}
260
+ nodesConnectable={false}
261
+ elementsSelectable={false}
262
+ edgesUpdatable={false}
263
+ connectOnClick={false}
264
+ >
265
+ <MiniMap
266
+ nodeColor={(n: Node): string => {
267
+ return (
268
+ (n.style as any)?.background ||
269
+ (n.data as any)?.color ||
270
+ "#ffffff"
271
+ );
272
+ }}
273
+ />
274
+ <Controls />
275
+ <Background gap={12} size={1} />
276
+ </ReactFlow>
277
+ </div>
278
+ );
279
+ };
280
+
281
+ export default ServiceDependencyGraph;
@@ -25,18 +25,19 @@ const TabElement: FunctionComponent<ComponentProps> = (
25
25
  ): ReactElement => {
26
26
  const backgroundColor: string = "bg-gray-100";
27
27
 
28
+ const baseClasses: string =
29
+ "rounded-md px-3 py-2 text-sm font-medium cursor-pointer inline-flex whitespace-nowrap flex-shrink-0";
30
+ const stateClasses: string = props.isSelected
31
+ ? `${backgroundColor} text-gray-700`
32
+ : "text-gray-500 hover:text-gray-700";
33
+
28
34
  return (
29
35
  <div className="mt-3 mb-3">
30
36
  <div
31
37
  data-testid={`tab-${props.tab.name}`}
32
38
  key={props.tab.name}
33
39
  onClick={props.onClick}
34
- className={`${
35
- (props.isSelected
36
- ? backgroundColor + " text-gray-700"
37
- : "text-gray-500 hover:text-gray-700") +
38
- " rounded-md px-3 py-2 text-sm font-medium cursor-pointer flex"
39
- }`}
40
+ className={`${stateClasses} ${baseClasses}`}
40
41
  aria-current={props.isSelected ? "page" : undefined}
41
42
  >
42
43
  <div>{props.tab.name}</div>
@@ -57,9 +58,7 @@ const TabElement: FunctionComponent<ComponentProps> = (
57
58
  >
58
59
  {props.tab.countBadge}
59
60
  </span>
60
- ) : (
61
- <></>
62
- )}
61
+ ) : null}
63
62
  </div>
64
63
  </div>
65
64
  );
@@ -28,22 +28,23 @@ const Tabs: FunctionComponent<ComponentProps> = (
28
28
 
29
29
  return (
30
30
  <div>
31
- <div className="hidden sm:block">
32
- <nav className="flex space-x-4" aria-label="Tabs">
33
- {props.tabs.map((tab: Tab) => {
34
- return (
35
- <TabElement
36
- key={tab.name}
37
- tab={tab}
38
- onClick={() => {
39
- setCurrentTab(tab);
40
- }}
41
- isSelected={tab === currentTab}
42
- />
43
- );
44
- })}
45
- </nav>
46
- </div>
31
+ <nav
32
+ className="flex space-x-2 overflow-x-auto md:overflow-visible md:space-x-4"
33
+ aria-label="Tabs"
34
+ >
35
+ {props.tabs.map((tab: Tab) => {
36
+ return (
37
+ <TabElement
38
+ key={tab.name}
39
+ tab={tab}
40
+ onClick={() => {
41
+ setCurrentTab(tab);
42
+ }}
43
+ isSelected={tab === currentTab}
44
+ />
45
+ );
46
+ })}
47
+ </nav>
47
48
  <div className="mt-3 ml-1">{currentTab && currentTab.children}</div>
48
49
  </div>
49
50
  );
@@ -134,7 +134,26 @@ export default class UptimeUtil {
134
134
  eventList[eventList.length - 1]!.endDate,
135
135
  )
136
136
  ) {
137
- if (monitorEvent.priority > eventList[eventList.length - 1]!.priority) {
137
+ let isEndDateOfCurrenteventAfterLastEvent: boolean = false;
138
+ if (
139
+ eventList[eventList.length - 1] &&
140
+ eventList[eventList.length - 1]?.endDate
141
+ ) {
142
+ isEndDateOfCurrenteventAfterLastEvent =
143
+ OneUptimeDate.isAfter(
144
+ monitorEvent.endDate,
145
+ eventList[eventList.length - 1]!.endDate,
146
+ ) ||
147
+ OneUptimeDate.isEqualBySeconds(
148
+ monitorEvent.endDate,
149
+ eventList[eventList.length - 1]!.endDate,
150
+ );
151
+ }
152
+
153
+ if (
154
+ monitorEvent.priority > eventList[eventList.length - 1]!.priority ||
155
+ isEndDateOfCurrenteventAfterLastEvent
156
+ ) {
138
157
  // end the last event at the start of this event.
139
158
 
140
159
  const tempLastEvent: Event = {