@oneuptime/common 11.0.10 → 11.0.13

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 (148) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +134 -0
  2. package/Models/DatabaseModels/CloudResource.ts +134 -0
  3. package/Models/DatabaseModels/DockerHost.ts +134 -0
  4. package/Models/DatabaseModels/DockerSwarmCluster.ts +134 -0
  5. package/Models/DatabaseModels/Host.ts +134 -0
  6. package/Models/DatabaseModels/KubernetesCluster.ts +134 -0
  7. package/Models/DatabaseModels/PodmanHost.ts +134 -0
  8. package/Models/DatabaseModels/ProxmoxCluster.ts +134 -0
  9. package/Models/DatabaseModels/RumApplication.ts +134 -0
  10. package/Models/DatabaseModels/ServerlessFunction.ts +134 -0
  11. package/Models/DatabaseModels/Service.ts +134 -0
  12. package/Models/DatabaseModels/StatusPage.ts +0 -42
  13. package/README.md +1 -1
  14. package/Scripts/generate-service-worker.js +46 -36
  15. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +4 -2
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.ts +331 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.ts +35 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Readme.md +3 -3
  20. package/Server/Infrastructure/Queue.ts +122 -0
  21. package/Server/Services/AIAgentService.ts +9 -4
  22. package/Server/Services/AnalyticsDatabaseService.ts +89 -56
  23. package/Server/Services/CephClusterService.ts +6 -4
  24. package/Server/Services/CloudResourceService.ts +6 -4
  25. package/Server/Services/DatabaseService.ts +126 -0
  26. package/Server/Services/DockerHostService.ts +6 -4
  27. package/Server/Services/DockerSwarmClusterService.ts +6 -4
  28. package/Server/Services/HostService.ts +6 -4
  29. package/Server/Services/KubernetesClusterService.ts +6 -4
  30. package/Server/Services/PodmanHostService.ts +6 -4
  31. package/Server/Services/ProbeService.ts +10 -4
  32. package/Server/Services/ProxmoxClusterService.ts +6 -4
  33. package/Server/Services/RumApplicationService.ts +6 -4
  34. package/Server/Services/ServerlessFunctionService.ts +6 -4
  35. package/Server/Services/ServiceService.ts +17 -4
  36. package/Server/Types/Markdown.ts +103 -0
  37. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +5 -2
  38. package/Server/Utils/CodeRepository/CodeRepository.ts +78 -0
  39. package/Server/Utils/Execute.ts +6 -0
  40. package/Server/Utils/Logger.ts +66 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +24 -10
  42. package/Server/Utils/Telemetry/EntityRegistry.ts +10 -5
  43. package/Server/Utils/Workspace/Slack/app-manifest.json +86 -92
  44. package/Tests/Server/Services/UpdateColumnsByIdWithoutHooks.test.ts +154 -0
  45. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +87 -9
  46. package/Tests/Types/DiskSize.test.ts +49 -0
  47. package/Types/Dashboard/DashboardComponents/DashboardTableComponent.ts +11 -0
  48. package/Types/Icon/IconProp.ts +1 -0
  49. package/Types/Monitor/MonitorType.ts +15 -31
  50. package/Types/Monitor/schema.json +32 -35
  51. package/UI/Components/BulkUpdate/BulkArchiveActions.tsx +150 -0
  52. package/UI/Components/Icon/Icon.tsx +42 -126
  53. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -5
  54. package/UI/esbuild-config.js +136 -110
  55. package/Utils/ClientAPI/README.md +1 -1
  56. package/build/dist/Models/DatabaseModels/CephCluster.js +136 -0
  57. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  58. package/build/dist/Models/DatabaseModels/CloudResource.js +136 -0
  59. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/DockerHost.js +136 -0
  61. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +136 -0
  63. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Host.js +136 -0
  65. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +136 -0
  67. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/PodmanHost.js +136 -0
  69. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +136 -0
  71. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/RumApplication.js +136 -0
  73. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  74. package/build/dist/Models/DatabaseModels/ServerlessFunction.js +136 -0
  75. package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/Service.js +136 -0
  77. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/StatusPage.js +0 -44
  79. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  80. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +4 -2
  81. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  82. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js +120 -0
  83. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782600000000-AddArchiveToResources.js.map +1 -0
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.js +28 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1782700000000-RemoveDeprecatedEnableSubscribersFromStatusPage.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  88. package/build/dist/Server/Infrastructure/Queue.js +63 -0
  89. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  90. package/build/dist/Server/Services/AIAgentService.js +9 -4
  91. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  92. package/build/dist/Server/Services/AnalyticsDatabaseService.js +52 -17
  93. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  94. package/build/dist/Server/Services/CephClusterService.js +6 -4
  95. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  96. package/build/dist/Server/Services/CloudResourceService.js +6 -4
  97. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  98. package/build/dist/Server/Services/DatabaseService.js +101 -0
  99. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  100. package/build/dist/Server/Services/DockerHostService.js +6 -4
  101. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  102. package/build/dist/Server/Services/DockerSwarmClusterService.js +6 -4
  103. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  104. package/build/dist/Server/Services/HostService.js +6 -4
  105. package/build/dist/Server/Services/HostService.js.map +1 -1
  106. package/build/dist/Server/Services/KubernetesClusterService.js +6 -4
  107. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  108. package/build/dist/Server/Services/PodmanHostService.js +6 -4
  109. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  110. package/build/dist/Server/Services/ProbeService.js +10 -4
  111. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  112. package/build/dist/Server/Services/ProxmoxClusterService.js +6 -4
  113. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  114. package/build/dist/Server/Services/RumApplicationService.js +6 -4
  115. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  116. package/build/dist/Server/Services/ServerlessFunctionService.js +6 -4
  117. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  118. package/build/dist/Server/Services/ServiceService.js +17 -4
  119. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  120. package/build/dist/Server/Types/Markdown.js +87 -0
  121. package/build/dist/Server/Types/Markdown.js.map +1 -1
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +3 -0
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/CodeRepository/CodeRepository.js +60 -0
  125. package/build/dist/Server/Utils/CodeRepository/CodeRepository.js.map +1 -1
  126. package/build/dist/Server/Utils/Execute.js +1 -3
  127. package/build/dist/Server/Utils/Execute.js.map +1 -1
  128. package/build/dist/Server/Utils/Logger.js +49 -1
  129. package/build/dist/Server/Utils/Logger.js.map +1 -1
  130. package/build/dist/Server/Utils/Monitor/MonitorResource.js +24 -10
  131. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  132. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +10 -5
  133. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  134. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +3 -9
  135. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTableComponent.js.map +1 -1
  136. package/build/dist/Types/Icon/IconProp.js +1 -0
  137. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorType.js +15 -31
  139. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  140. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js +96 -0
  141. package/build/dist/UI/Components/BulkUpdate/BulkArchiveActions.js.map +1 -0
  142. package/build/dist/UI/Components/Icon/Icon.js +17 -41
  143. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  144. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -3
  145. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  146. package/jest.config.json +47 -66
  147. package/package.json +1 -1
  148. package/tsconfig.json +109 -113
@@ -1,97 +1,91 @@
1
1
  {
2
- "display_information": {
3
- "name": "OneUptime",
4
- "description": "The Complete Open-Source Observability Platform",
5
- "background_color": "#000000",
6
- "long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
7
- },
2
+ "display_information": {
3
+ "name": "OneUptime",
4
+ "description": "The Complete Open-Source Observability Platform",
5
+ "background_color": "#000000",
6
+ "long_description": "OneUptime is a comprehensive solution for monitoring and managing your online services. Whether you need to check the availability of your website, dashboard, API, or any other online resource, OneUptime can alert your team when downtime happens and keep your customers informed with a status page. OneUptime also helps you handle incidents, set up on-call rotations, run tests, secure your services, analyze logs, track performance, and debug errors."
7
+ },
8
8
 
9
- "features": {
10
- "app_home": {
11
- "home_tab_enabled": false,
12
- "messages_tab_enabled": true,
13
- "messages_tab_read_only_enabled": false
14
- },
15
- "bot_user": {
16
- "display_name": "OneUptime",
17
- "always_online": true
18
- },
19
- "shortcuts": [
20
- {
21
- "name": "Create Scheduled Event",
22
- "type": "global",
23
- "callback_id": "/maintenance",
24
- "description": "Create a new scheduled event in OneUptime"
25
- },
26
- {
27
- "name": "Create New Incident",
28
- "type": "global",
29
- "callback_id": "/incident",
30
- "description": "Creates a new incident in OneUptime"
31
- }
32
- ],
33
- "slash_commands": [
34
- {
35
- "command": "/incident",
36
- "url": "{{SERVER_URL}}/api/slack/interactive",
37
- "description": "Create a new Incident in OneUptime",
38
- "usage_hint": "[incident title]",
39
- "should_escape": false
40
- },
41
- {
42
- "command": "/maintenance",
43
- "url": "{{SERVER_URL}}/api/slack/interactive",
44
- "description": "Create a new Scheduled Maintenance in OneUptime",
45
- "usage_hint": "[maintenance title]",
46
- "should_escape": false
47
- }
48
- ]
9
+ "features": {
10
+ "app_home": {
11
+ "home_tab_enabled": false,
12
+ "messages_tab_enabled": true,
13
+ "messages_tab_read_only_enabled": false
14
+ },
15
+ "bot_user": {
16
+ "display_name": "OneUptime",
17
+ "always_online": true
49
18
  },
50
- "oauth_config": {
51
- "redirect_urls": [
52
- "{{SERVER_URL}}/api/slack/auth"
53
- ],
54
- "scopes": {
55
- "user": [
56
- "users:read"
57
- ],
58
- "bot": [
59
- "commands",
60
- "channels:history",
61
- "channels:join",
62
- "channels:manage",
63
- "channels:read",
64
- "channels:write.invites",
65
- "channels:write.topic",
66
- "chat:write",
67
- "users:read",
68
- "groups:read",
69
- "groups:write",
70
- "groups:history",
71
- "im:read",
72
- "im:write",
73
- "im:history",
74
- "mpim:read",
75
- "mpim:write",
76
- "mpim:history",
77
- "reactions:read"
78
- ]
79
- }
19
+ "shortcuts": [
20
+ {
21
+ "name": "Create Scheduled Event",
22
+ "type": "global",
23
+ "callback_id": "/maintenance",
24
+ "description": "Create a new scheduled event in OneUptime"
25
+ },
26
+ {
27
+ "name": "Create New Incident",
28
+ "type": "global",
29
+ "callback_id": "/incident",
30
+ "description": "Creates a new incident in OneUptime"
31
+ }
32
+ ],
33
+ "slash_commands": [
34
+ {
35
+ "command": "/incident",
36
+ "url": "{{SERVER_URL}}/api/slack/interactive",
37
+ "description": "Create a new Incident in OneUptime",
38
+ "usage_hint": "[incident title]",
39
+ "should_escape": false
40
+ },
41
+ {
42
+ "command": "/maintenance",
43
+ "url": "{{SERVER_URL}}/api/slack/interactive",
44
+ "description": "Create a new Scheduled Maintenance in OneUptime",
45
+ "usage_hint": "[maintenance title]",
46
+ "should_escape": false
47
+ }
48
+ ]
49
+ },
50
+ "oauth_config": {
51
+ "redirect_urls": ["{{SERVER_URL}}/api/slack/auth"],
52
+ "scopes": {
53
+ "user": ["users:read"],
54
+ "bot": [
55
+ "commands",
56
+ "channels:history",
57
+ "channels:join",
58
+ "channels:manage",
59
+ "channels:read",
60
+ "channels:write.invites",
61
+ "channels:write.topic",
62
+ "chat:write",
63
+ "users:read",
64
+ "groups:read",
65
+ "groups:write",
66
+ "groups:history",
67
+ "im:read",
68
+ "im:write",
69
+ "im:history",
70
+ "mpim:read",
71
+ "mpim:write",
72
+ "mpim:history",
73
+ "reactions:read"
74
+ ]
75
+ }
76
+ },
77
+ "settings": {
78
+ "interactivity": {
79
+ "is_enabled": true,
80
+ "request_url": "{{SERVER_URL}}/api/slack/interactive",
81
+ "message_menu_options_url": "{{SERVER_URL}}/api/slack/options-load"
80
82
  },
81
- "settings": {
82
- "interactivity": {
83
- "is_enabled": true,
84
- "request_url": "{{SERVER_URL}}/api/slack/interactive",
85
- "message_menu_options_url": "{{SERVER_URL}}/api/slack/options-load"
86
- },
87
- "org_deploy_enabled": true,
88
- "socket_mode_enabled": false,
89
- "token_rotation_enabled": false,
90
- "event_subscriptions": {
91
- "request_url": "{{SERVER_URL}}/api/slack/events",
92
- "bot_events": [
93
- "reaction_added"
94
- ]
95
- }
83
+ "org_deploy_enabled": true,
84
+ "socket_mode_enabled": false,
85
+ "token_rotation_enabled": false,
86
+ "event_subscriptions": {
87
+ "request_url": "{{SERVER_URL}}/api/slack/events",
88
+ "bot_events": ["reaction_added"]
96
89
  }
97
- }
90
+ }
91
+ }
@@ -0,0 +1,154 @@
1
+ import ServiceService from "../../../Server/Services/ServiceService";
2
+ import ObjectID from "../../../Types/ObjectID";
3
+ import { describe, expect, test, afterEach } from "@jest/globals";
4
+
5
+ /*
6
+ * DatabaseService.updateColumnsByIdWithoutHooks is the fast heartbeat write
7
+ * path behind ServiceService.updateLastSeen. It exists to keep the single
8
+ * hottest UPDATE in the system (lastSeenAt, bumped on every telemetry batch)
9
+ * out of the full updateOneById pipeline, which holds the row's write lock
10
+ * across several round trips and was observed heading a Postgres lock convoy
11
+ * that starved the connection pool.
12
+ *
13
+ * These tests mock the TypeORM manager (no Postgres) and lock in the two
14
+ * invariants that make the fix work:
15
+ * - it emits exactly ONE parameterized UPDATE, and
16
+ * - it NEVER rewrites the optimistic-lock `version` column (a regression
17
+ * here would re-introduce the contention the fix removed).
18
+ */
19
+
20
+ type QueryCall = [string, Array<unknown>];
21
+
22
+ const SERVICE_COLUMNS: Record<string, string> = {
23
+ lastSeenAt: "lastSeenAt",
24
+ serviceVersion: "serviceVersion",
25
+ deploymentEnvironment: "deploymentEnvironment",
26
+ };
27
+
28
+ /*
29
+ * Stand-in for the TypeORM driver's persist path. The real method applies
30
+ * column transformers (ObjectID -> uuid string, JSON.stringify, etc.); we
31
+ * model that here by wrapping a string value in `prepared(...)` so a test
32
+ * can assert the column value was routed through it rather than bound raw.
33
+ */
34
+ function fakePreparePersistentValue(value: unknown): unknown {
35
+ return typeof value === "string" ? `prepared(${value})` : value;
36
+ }
37
+
38
+ function mockRepository(result: unknown = []): jest.Mock {
39
+ const query: jest.Mock = jest.fn().mockResolvedValue(result);
40
+ jest.spyOn(ServiceService, "getRepository").mockReturnValue({
41
+ manager: {
42
+ query,
43
+ connection: {
44
+ driver: { preparePersistentValue: fakePreparePersistentValue },
45
+ },
46
+ },
47
+ metadata: {
48
+ tableName: "Service",
49
+ primaryColumns: [{ databaseName: "_id" }],
50
+ updateDateColumn: { databaseName: "updatedAt" },
51
+ findColumnWithPropertyName: (propertyName: string) => {
52
+ return SERVICE_COLUMNS[propertyName]
53
+ ? { databaseName: SERVICE_COLUMNS[propertyName] }
54
+ : undefined;
55
+ },
56
+ },
57
+ } as any);
58
+ return query;
59
+ }
60
+
61
+ describe("DatabaseService.updateColumnsByIdWithoutHooks", () => {
62
+ afterEach(() => {
63
+ jest.restoreAllMocks();
64
+ });
65
+
66
+ test("emits a single UPDATE and never bumps the version column", async () => {
67
+ const query: jest.Mock = mockRepository();
68
+ const id: ObjectID = ObjectID.generate();
69
+ const lastSeenAt: Date = new Date();
70
+
71
+ await ServiceService.updateColumnsByIdWithoutHooks({
72
+ id,
73
+ data: { lastSeenAt } as never,
74
+ });
75
+
76
+ expect(query).toHaveBeenCalledTimes(1);
77
+ const [sql, params] = query.mock.calls[0] as QueryCall;
78
+
79
+ // The whole point of the fix: no optimistic-lock churn on the hot row.
80
+ expect(sql).not.toContain("version");
81
+ // updatedAt is still refreshed, exactly as the normal update path does.
82
+ expect(sql).toContain(`"updatedAt" = CURRENT_TIMESTAMP`);
83
+ expect(sql).toBe(
84
+ `UPDATE "Service" SET "lastSeenAt" = $1, "updatedAt" = CURRENT_TIMESTAMP WHERE "_id" = $2`,
85
+ );
86
+ expect(params).toEqual([lastSeenAt, id.toString()]);
87
+ });
88
+
89
+ test("maps multiple entity properties to columns in stable param order", async () => {
90
+ const query: jest.Mock = mockRepository();
91
+ const id: ObjectID = ObjectID.generate();
92
+ const lastSeenAt: Date = new Date();
93
+
94
+ await ServiceService.updateColumnsByIdWithoutHooks({
95
+ id,
96
+ data: {
97
+ lastSeenAt,
98
+ serviceVersion: "1.2.3",
99
+ deploymentEnvironment: "production",
100
+ } as never,
101
+ });
102
+
103
+ const [sql, params] = query.mock.calls[0] as QueryCall;
104
+ expect(sql).toBe(
105
+ `UPDATE "Service" SET "lastSeenAt" = $1, "serviceVersion" = $2, ` +
106
+ `"deploymentEnvironment" = $3, "updatedAt" = CURRENT_TIMESTAMP WHERE "_id" = $4`,
107
+ );
108
+ /*
109
+ * Column values are routed through the driver's persist path (here the
110
+ * `prepared(...)` stand-in), so transformers/JSON/coercion apply exactly
111
+ * as save() would. The `_id` in the WHERE clause is bound raw.
112
+ */
113
+ expect(params).toEqual([
114
+ lastSeenAt,
115
+ "prepared(1.2.3)",
116
+ "prepared(production)",
117
+ id.toString(),
118
+ ]);
119
+ });
120
+
121
+ test("throws on an unknown column instead of silently writing nothing", async () => {
122
+ mockRepository();
123
+ await expect(
124
+ ServiceService.updateColumnsByIdWithoutHooks({
125
+ id: ObjectID.generate(),
126
+ data: { notAColumn: "x" } as never,
127
+ }),
128
+ ).rejects.toThrow(/unknown column "notAColumn"/);
129
+ });
130
+
131
+ test("rejects SQL-expression (function) values it cannot bind", async () => {
132
+ const query: jest.Mock = mockRepository();
133
+ await expect(
134
+ ServiceService.updateColumnsByIdWithoutHooks({
135
+ id: ObjectID.generate(),
136
+ data: {
137
+ lastSeenAt: (() => {
138
+ return "NOW()";
139
+ }) as never,
140
+ } as never,
141
+ }),
142
+ ).rejects.toThrow(/SQL-expression values are not supported/);
143
+ expect(query).not.toHaveBeenCalled();
144
+ });
145
+
146
+ test("does not issue a query when there are no columns to update", async () => {
147
+ const query: jest.Mock = mockRepository();
148
+ await ServiceService.updateColumnsByIdWithoutHooks({
149
+ id: ObjectID.generate(),
150
+ data: {} as never,
151
+ });
152
+ expect(query).not.toHaveBeenCalled();
153
+ });
154
+ });
@@ -111,12 +111,15 @@ describe("StatementGenerator", () => {
111
111
  expect(jest.mocked(logger.debug)).toHaveBeenNthCalledWith(2, statement);
112
112
 
113
113
  /* eslint-disable prettier/prettier */
114
- expectStatement(statement, SQL`
115
- ALTER TABLE ${'oneuptime'}.${'<table-name>'}
114
+ expectStatement(
115
+ statement,
116
+ SQL`
117
+ ALTER TABLE ${"oneuptime"}.${"<table-name>"}
116
118
  UPDATE <set-statement>
117
119
  WHERE TRUE <where-statement>
118
- `);
119
- /* eslint-enable prettier/prettier */
120
+ `,
121
+ );
122
+ /* eslint-enable prettier/prettier */
120
123
  });
121
124
  });
122
125
 
@@ -633,6 +636,81 @@ describe("StatementGenerator", () => {
633
636
  });
634
637
  });
635
638
 
639
+ describe("toCreateStatement", () => {
640
+ /*
641
+ * Regression test for the escapeStringLiteral undefined/null guard.
642
+ * An ArrayText (Array(String)) column whose value array carried
643
+ * undefined/null members at runtime used to throw
644
+ * "TypeError: Cannot read properties of undefined (reading 'replace')"
645
+ * inside escapeStringLiteral while building the INSERT. The member is
646
+ * now rendered as the empty string literal '' so the statement still
647
+ * serializes to a valid ClickHouse Array(String).
648
+ */
649
+ class ArrayTextModel extends AnalyticsBaseModel {
650
+ public constructor() {
651
+ super({
652
+ tableName: "<array-create-table>",
653
+ singularName: "<singular>",
654
+ pluralName: "<plural>",
655
+ tableColumns: [
656
+ new AnalyticsTableColumn({
657
+ key: "_id",
658
+ title: "<title>",
659
+ description: "<description>",
660
+ required: true,
661
+ type: TableColumnType.ObjectID,
662
+ }),
663
+ new AnalyticsTableColumn({
664
+ key: "entityKeys",
665
+ title: "<title>",
666
+ description: "<description>",
667
+ required: true,
668
+ defaultValue: [],
669
+ type: TableColumnType.ArrayText,
670
+ }),
671
+ ],
672
+ crudApiPath: new Route("route"),
673
+ primaryKeys: ["_id"],
674
+ sortKeys: ["_id"],
675
+ partitionKey: "_id",
676
+ tableEngine: AnalyticsTableEngine.MergeTree,
677
+ });
678
+ }
679
+ }
680
+
681
+ let arrayTextGenerator: StatementGenerator<ArrayTextModel>;
682
+ beforeEach(() => {
683
+ arrayTextGenerator = new StatementGenerator<ArrayTextModel>({
684
+ modelType: ArrayTextModel,
685
+ database: ClickhouseAppInstance,
686
+ });
687
+ });
688
+
689
+ test("renders undefined/null ArrayText elements as '' instead of throwing", () => {
690
+ const item: ArrayTextModel = new ArrayTextModel();
691
+ item.setColumnValue("_id", "210dac24142f1baa");
692
+ /*
693
+ * Simulate runtime data where the Array(String) column carries
694
+ * undefined/null members. The type system models the column as
695
+ * string[], hence the cast — this is exactly what crashed before
696
+ * the guard was added.
697
+ */
698
+ item.setColumnValue("entityKeys", [
699
+ "alpha",
700
+ undefined,
701
+ null,
702
+ "beta",
703
+ ] as unknown as Array<string>);
704
+
705
+ let statement: string = "";
706
+ expect(() => {
707
+ statement = arrayTextGenerator.toCreateStatement({ item: [item] });
708
+ }).not.toThrow();
709
+
710
+ expect(statement).toContain("['alpha', '', '', 'beta']");
711
+ });
712
+ });
713
+
636
714
  describe("toSelectStatement", () => {
637
715
  test("should return the contents of a SELECT statement", () => {
638
716
  const { statement, columns } = generator.toSelectStatement({
@@ -790,18 +868,18 @@ describe("StatementGenerator", () => {
790
868
  expect(jest.mocked(logger.debug)).toHaveBeenNthCalledWith(2, statement);
791
869
 
792
870
  /* eslint-disable prettier/prettier */
793
- const expectedStatement: Statement = SQL`
794
- CREATE TABLE IF NOT EXISTS ${'oneuptime'}.${'<table-name>'}
871
+ const expectedStatement: Statement = SQL`
872
+ CREATE TABLE IF NOT EXISTS ${"oneuptime"}.${"<table-name>"}
795
873
  (
796
874
  <columns-create-statement>
797
875
  )
798
876
  ENGINE = MergeTree
799
877
  PARTITION BY (column_ObjectID)
800
878
 
801
- PRIMARY KEY (${'column_ObjectID'})
802
- ORDER BY (${'column_ObjectID'})
879
+ PRIMARY KEY (${"column_ObjectID"})
880
+ ORDER BY (${"column_ObjectID"})
803
881
  `;
804
- /* eslint-enable prettier/prettier */
882
+ /* eslint-enable prettier/prettier */
805
883
 
806
884
  // Normalize whitespace for comparison to avoid formatting issues
807
885
  const normalizeWhitespace: (s: string) => string = (
@@ -0,0 +1,49 @@
1
+ import BadDataException from "../../Types/Exception/BadDataException";
2
+ import DiskSize from "../../Types/DiskSize";
3
+
4
+ describe("DiskSize", () => {
5
+ describe("convertToDecimalPlaces", () => {
6
+ test("rounds to 2 decimal places by default", () => {
7
+ expect(DiskSize.convertToDecimalPlaces(3.14159)).toBe(3.14);
8
+ });
9
+
10
+ test("rounds to the requested number of decimal places", () => {
11
+ expect(DiskSize.convertToDecimalPlaces(3.14159, 3)).toBe(3.142);
12
+ });
13
+
14
+ test("returns the ceiling when decimalPlaces is 0", () => {
15
+ expect(DiskSize.convertToDecimalPlaces(4.2, 0)).toBe(5);
16
+ expect(DiskSize.convertToDecimalPlaces(4, 0)).toBe(4);
17
+ });
18
+
19
+ test("parses a string value before rounding", () => {
20
+ expect(
21
+ DiskSize.convertToDecimalPlaces("2.71828" as unknown as number),
22
+ ).toBe(2.72);
23
+ });
24
+
25
+ test("throws a BadDataException when decimalPlaces is negative", () => {
26
+ expect(() => {
27
+ return DiskSize.convertToDecimalPlaces(1.5, -1);
28
+ }).toThrow(BadDataException);
29
+ });
30
+ });
31
+
32
+ describe("byteSizeToGB", () => {
33
+ test("converts bytes to gigabytes", () => {
34
+ expect(DiskSize.byteSizeToGB(1024 * 1024 * 1024)).toBe(1);
35
+ });
36
+ });
37
+
38
+ describe("byteSizeToMB", () => {
39
+ test("converts bytes to megabytes", () => {
40
+ expect(DiskSize.byteSizeToMB(1024 * 1024)).toBe(1);
41
+ });
42
+ });
43
+
44
+ describe("byteSizeToKB", () => {
45
+ test("converts bytes to kilobytes", () => {
46
+ expect(DiskSize.byteSizeToKB(1024)).toBe(1);
47
+ });
48
+ });
49
+ });
@@ -2,6 +2,8 @@ import MetricFormulaConfigData from "../../Metrics/MetricFormulaConfigData";
2
2
  import MetricQueryConfigData from "../../Metrics/MetricQueryConfigData";
3
3
  import MetricsAggregationType from "../../Metrics/MetricsAggregationType";
4
4
  import ObjectID from "../../ObjectID";
5
+ import Dictionary from "../../Dictionary";
6
+ import { DictionaryEntryValue } from "../../../UI/Components/Dictionary/DictionaryFilterOperator";
5
7
  import DashboardComponentType from "../DashboardComponentType";
6
8
  import BaseComponent from "./DashboardBaseComponent";
7
9
 
@@ -76,6 +78,15 @@ export default interface DashboardTableComponent extends BaseComponent {
76
78
  maxRows?: number | undefined;
77
79
  reduce?: TableReduce | undefined;
78
80
  decimals?: number | undefined;
81
+ /*
82
+ * Widget-level attribute filter applied to EVERY metric column's query
83
+ * (e.g. deployment.environment = production, so the table only shows
84
+ * hosts/services in that environment). Stored in the same operator-aware
85
+ * shape the chart widget writes into filterData.attributes, so it flows
86
+ * straight through MetricUtil.fetchResults' sanitizeAttributeFilters with
87
+ * no backend changes. Empty/undefined = no filter (every series shown).
88
+ */
89
+ attributeFilters?: Dictionary<DictionaryEntryValue> | undefined;
79
90
  /*
80
91
  * Legacy widget shape from before the columns/groupByAttributes
81
92
  * redesign — read-only at render time, never written by the editor.
@@ -324,6 +324,7 @@ enum IconProp {
324
324
  XCircle = "XCircle",
325
325
  Kubernetes = "Kubernetes",
326
326
  Docker = "Docker",
327
+ DockerSwarm = "DockerSwarm",
327
328
  Podman = "Podman",
328
329
  Proxmox = "Proxmox",
329
330
  Ceph = "Ceph",
@@ -93,36 +93,20 @@ export class MonitorTypeHelper {
93
93
  monitorTypes: [MonitorType.IncomingRequest, MonitorType.IncomingEmail],
94
94
  },
95
95
  {
96
- label: "Infrastructure",
96
+ label: "Network",
97
97
  monitorTypes: [MonitorType.SNMP],
98
98
  },
99
99
  {
100
- label: "Kubernetes",
101
- monitorTypes: [MonitorType.Kubernetes],
102
- },
103
- {
104
- label: "Docker",
105
- monitorTypes: [MonitorType.Docker],
106
- },
107
- {
108
- label: "Host",
109
- monitorTypes: [MonitorType.Host],
110
- },
111
- {
112
- label: "Podman",
113
- monitorTypes: [MonitorType.Podman],
114
- },
115
- {
116
- label: "Docker Swarm",
117
- monitorTypes: [MonitorType.DockerSwarm],
118
- },
119
- {
120
- label: "Proxmox",
121
- monitorTypes: [MonitorType.Proxmox],
122
- },
123
- {
124
- label: "Ceph",
125
- monitorTypes: [MonitorType.Ceph],
100
+ label: "Infrastructure",
101
+ monitorTypes: [
102
+ MonitorType.Host,
103
+ MonitorType.Kubernetes,
104
+ MonitorType.Docker,
105
+ MonitorType.Podman,
106
+ MonitorType.DockerSwarm,
107
+ MonitorType.Proxmox,
108
+ MonitorType.Ceph,
109
+ ],
126
110
  },
127
111
  {
128
112
  label: "Telemetry",
@@ -225,28 +209,28 @@ export class MonitorTypeHelper {
225
209
  title: "Podman Container",
226
210
  description:
227
211
  "This monitor type lets you monitor Podman containers — CPU, memory, network, restarts, and lifecycle events.",
228
- icon: IconProp.Cube,
212
+ icon: IconProp.Podman,
229
213
  },
230
214
  {
231
215
  monitorType: MonitorType.DockerSwarm,
232
216
  title: "Docker Swarm",
233
217
  description:
234
218
  "This monitor type lets you monitor Docker Swarm clusters — per-task container CPU, memory, network, process count, and uptime across the cluster's services and nodes.",
235
- icon: IconProp.Cube,
219
+ icon: IconProp.DockerSwarm,
236
220
  },
237
221
  {
238
222
  monitorType: MonitorType.Proxmox,
239
223
  title: "Proxmox",
240
224
  description:
241
225
  "This monitor type lets you monitor Proxmox VE clusters — node availability, VM and container (guest) health, CPU, memory, storage, and HA state.",
242
- icon: IconProp.ServerStack,
226
+ icon: IconProp.Proxmox,
243
227
  },
244
228
  {
245
229
  monitorType: MonitorType.Ceph,
246
230
  title: "Ceph",
247
231
  description:
248
232
  "This monitor type lets you monitor Ceph storage clusters — cluster health, monitor quorum, OSD availability, pool capacity, and placement group states.",
249
- icon: IconProp.Database,
233
+ icon: IconProp.Ceph,
250
234
  },
251
235
  {
252
236
  monitorType: MonitorType.IP,