@oneuptime/common 10.5.1 → 10.5.3

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 (77) hide show
  1. package/Models/DatabaseModels/StatusPageGroup.ts +212 -0
  2. package/Models/DatabaseModels/StatusPageResource.ts +86 -0
  3. package/Models/DatabaseModels/TelemetryException.ts +10 -0
  4. package/Server/API/StatusPageAPI.ts +15 -0
  5. package/Server/API/TelemetryAPI.ts +406 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.ts +68 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.ts +65 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +5 -0
  9. package/Server/Services/ExceptionAggregationService.ts +51 -3
  10. package/Server/Services/LogAggregationService.ts +1 -0
  11. package/Server/Services/MetricAggregationService.ts +227 -0
  12. package/Server/Services/OpenTelemetryIngestService.ts +101 -1
  13. package/Server/Services/StatusPageService.ts +5 -0
  14. package/Server/Services/TraceAggregationService.ts +1 -0
  15. package/Server/Utils/Monitor/MonitorLogUtil.ts +146 -6
  16. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +299 -0
  17. package/Types/Monitor/MonitorStep.ts +85 -0
  18. package/Types/StatusPage/StatusPageGroupViewMode.ts +6 -0
  19. package/UI/Components/Accordion/Accordion.tsx +32 -26
  20. package/UI/Components/LogsViewer/LogsViewer.tsx +10 -0
  21. package/UI/Components/LogsViewer/components/FacetSection.tsx +40 -3
  22. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +23 -0
  23. package/UI/Components/LogsViewer/types.ts +2 -0
  24. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +8 -0
  25. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +49 -3
  26. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +16 -0
  27. package/UI/Components/TelemetryViewer/types.ts +12 -0
  28. package/build/dist/Models/DatabaseModels/StatusPageGroup.js +217 -0
  29. package/build/dist/Models/DatabaseModels/StatusPageGroup.js.map +1 -1
  30. package/build/dist/Models/DatabaseModels/StatusPageResource.js +88 -0
  31. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  32. package/build/dist/Models/DatabaseModels/TelemetryException.js +11 -0
  33. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  34. package/build/dist/Server/API/StatusPageAPI.js +15 -0
  35. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  36. package/build/dist/Server/API/TelemetryAPI.js +285 -0
  37. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  38. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js +42 -0
  39. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js.map +1 -0
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js +28 -0
  41. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js.map +1 -0
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  44. package/build/dist/Server/Services/ExceptionAggregationService.js +44 -4
  45. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
  46. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  47. package/build/dist/Server/Services/MetricAggregationService.js +159 -0
  48. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -0
  49. package/build/dist/Server/Services/OpenTelemetryIngestService.js +60 -3
  50. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  51. package/build/dist/Server/Services/StatusPageService.js +5 -0
  52. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  53. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  54. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +127 -4
  55. package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
  56. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +204 -0
  57. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -0
  58. package/build/dist/Types/Monitor/MonitorStep.js +59 -0
  59. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  60. package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js +7 -0
  61. package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js.map +1 -0
  62. package/build/dist/UI/Components/Accordion/Accordion.js +11 -11
  63. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  64. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +1 -1
  65. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  66. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +26 -6
  67. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  68. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +12 -1
  69. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  70. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  71. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  72. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  73. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +32 -6
  74. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
  75. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +6 -1
  76. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,299 @@
1
+ import ObjectID from "../../../Types/ObjectID";
2
+ import PositiveNumber from "../../../Types/PositiveNumber";
3
+ import Search from "../../../Types/BaseDatabase/Search";
4
+ import MultiSearch from "../../../Types/BaseDatabase/MultiSearch";
5
+ import ServiceModel from "../../../Models/DatabaseModels/Service";
6
+ import HostModel from "../../../Models/DatabaseModels/Host";
7
+ import DockerHostModel from "../../../Models/DatabaseModels/DockerHost";
8
+ import KubernetesClusterModel from "../../../Models/DatabaseModels/KubernetesCluster";
9
+ import ServiceService from "../../Services/ServiceService";
10
+ import HostService from "../../Services/HostService";
11
+ import DockerHostService from "../../Services/DockerHostService";
12
+ import KubernetesClusterService from "../../Services/KubernetesClusterService";
13
+ import CaptureSpan from "./CaptureSpan";
14
+
15
+ /*
16
+ * Facet keys whose values are entity IDs backed by a Postgres source-of-truth
17
+ * table. ClickHouse aggregation only sees IDs that appear in the sampled
18
+ * trace/log window, so low-volume resources never reach the filter sidebar.
19
+ * Resolving the value list from Postgres instead means every project resource
20
+ * shows up regardless of recent telemetry activity, and the sidebar search
21
+ * matches across the full set (not just the loaded subset).
22
+ */
23
+ export const RESOURCE_FACET_KEYS: ReadonlySet<string> = new Set([
24
+ "serviceId",
25
+ "hostId",
26
+ "dockerHostId",
27
+ "kubernetesClusterId",
28
+ ]);
29
+
30
+ export interface ResourceFacetSpec {
31
+ facetKey: string;
32
+ counts: Map<string, number>;
33
+ searchText?: string | undefined;
34
+ limit?: number | undefined;
35
+ }
36
+
37
+ export interface ResolvedFacetValue {
38
+ value: string;
39
+ count: number;
40
+ displayName: string;
41
+ }
42
+
43
+ export default class ResourceFacetResolver {
44
+ private static readonly DEFAULT_LIMIT: number = 500;
45
+
46
+ public static isResourceFacet(facetKey: string): boolean {
47
+ return RESOURCE_FACET_KEYS.has(facetKey);
48
+ }
49
+
50
+ @CaptureSpan()
51
+ public static async resolve(
52
+ projectId: ObjectID,
53
+ specs: Array<ResourceFacetSpec>,
54
+ ): Promise<Record<string, Array<ResolvedFacetValue>>> {
55
+ const results: Array<readonly [string, Array<ResolvedFacetValue>]> =
56
+ await Promise.all(
57
+ specs.map(
58
+ async (
59
+ spec: ResourceFacetSpec,
60
+ ): Promise<readonly [string, Array<ResolvedFacetValue>]> => {
61
+ try {
62
+ const values: Array<ResolvedFacetValue> =
63
+ await ResourceFacetResolver.resolveOne(projectId, spec);
64
+ return [spec.facetKey, values] as const;
65
+ } catch {
66
+ return [spec.facetKey, []] as const;
67
+ }
68
+ },
69
+ ),
70
+ );
71
+
72
+ return Object.fromEntries(results);
73
+ }
74
+
75
+ private static async resolveOne(
76
+ projectId: ObjectID,
77
+ spec: ResourceFacetSpec,
78
+ ): Promise<Array<ResolvedFacetValue>> {
79
+ const limit: number = spec.limit ?? ResourceFacetResolver.DEFAULT_LIMIT;
80
+ const searchText: string | undefined =
81
+ spec.searchText && spec.searchText.trim().length > 0
82
+ ? spec.searchText.trim()
83
+ : undefined;
84
+
85
+ switch (spec.facetKey) {
86
+ case "serviceId":
87
+ return ResourceFacetResolver.queryServices(
88
+ projectId,
89
+ spec.counts,
90
+ searchText,
91
+ limit,
92
+ );
93
+ case "hostId":
94
+ return ResourceFacetResolver.queryHosts(
95
+ projectId,
96
+ spec.counts,
97
+ searchText,
98
+ limit,
99
+ );
100
+ case "dockerHostId":
101
+ return ResourceFacetResolver.queryDockerHosts(
102
+ projectId,
103
+ spec.counts,
104
+ searchText,
105
+ limit,
106
+ );
107
+ case "kubernetesClusterId":
108
+ return ResourceFacetResolver.queryKubernetesClusters(
109
+ projectId,
110
+ spec.counts,
111
+ searchText,
112
+ limit,
113
+ );
114
+ default:
115
+ return [];
116
+ }
117
+ }
118
+
119
+ private static async queryServices(
120
+ projectId: ObjectID,
121
+ counts: Map<string, number>,
122
+ searchText: string | undefined,
123
+ limit: number,
124
+ ): Promise<Array<ResolvedFacetValue>> {
125
+ const query: Record<string, unknown> = { projectId };
126
+ if (searchText) {
127
+ query["name"] = new Search<string>(searchText);
128
+ }
129
+
130
+ const services: Array<ServiceModel> = await ServiceService.findBy({
131
+ query: query as any,
132
+ select: {
133
+ _id: true,
134
+ name: true,
135
+ },
136
+ limit: new PositiveNumber(limit),
137
+ skip: new PositiveNumber(0),
138
+ props: { isRoot: true },
139
+ });
140
+
141
+ return ResourceFacetResolver.mergeCounts(
142
+ services.map((s: ServiceModel): { id: string; displayName: string } => {
143
+ return {
144
+ id: s._id ? s._id.toString() : "",
145
+ displayName: s.name || "Unknown",
146
+ };
147
+ }),
148
+ counts,
149
+ );
150
+ }
151
+
152
+ private static async queryHosts(
153
+ projectId: ObjectID,
154
+ counts: Map<string, number>,
155
+ searchText: string | undefined,
156
+ limit: number,
157
+ ): Promise<Array<ResolvedFacetValue>> {
158
+ const query: Record<string, unknown> = { projectId };
159
+ if (searchText) {
160
+ query["name"] = new MultiSearch({
161
+ fields: ["name", "hostIdentifier"],
162
+ value: searchText,
163
+ });
164
+ }
165
+
166
+ const hosts: Array<HostModel> = await HostService.findBy({
167
+ query: query as any,
168
+ select: {
169
+ _id: true,
170
+ name: true,
171
+ hostIdentifier: true,
172
+ },
173
+ limit: new PositiveNumber(limit),
174
+ skip: new PositiveNumber(0),
175
+ props: { isRoot: true },
176
+ });
177
+
178
+ return ResourceFacetResolver.mergeCounts(
179
+ hosts.map((h: HostModel): { id: string; displayName: string } => {
180
+ return {
181
+ id: h._id ? h._id.toString() : "",
182
+ displayName: h.name || h.hostIdentifier || "Unknown",
183
+ };
184
+ }),
185
+ counts,
186
+ );
187
+ }
188
+
189
+ private static async queryDockerHosts(
190
+ projectId: ObjectID,
191
+ counts: Map<string, number>,
192
+ searchText: string | undefined,
193
+ limit: number,
194
+ ): Promise<Array<ResolvedFacetValue>> {
195
+ const query: Record<string, unknown> = { projectId };
196
+ if (searchText) {
197
+ query["name"] = new MultiSearch({
198
+ fields: ["name", "hostIdentifier"],
199
+ value: searchText,
200
+ });
201
+ }
202
+
203
+ const dockerHosts: Array<DockerHostModel> = await DockerHostService.findBy({
204
+ query: query as any,
205
+ select: {
206
+ _id: true,
207
+ name: true,
208
+ hostIdentifier: true,
209
+ },
210
+ limit: new PositiveNumber(limit),
211
+ skip: new PositiveNumber(0),
212
+ props: { isRoot: true },
213
+ });
214
+
215
+ return ResourceFacetResolver.mergeCounts(
216
+ dockerHosts.map(
217
+ (d: DockerHostModel): { id: string; displayName: string } => {
218
+ return {
219
+ id: d._id ? d._id.toString() : "",
220
+ displayName: d.name || d.hostIdentifier || "Unknown",
221
+ };
222
+ },
223
+ ),
224
+ counts,
225
+ );
226
+ }
227
+
228
+ private static async queryKubernetesClusters(
229
+ projectId: ObjectID,
230
+ counts: Map<string, number>,
231
+ searchText: string | undefined,
232
+ limit: number,
233
+ ): Promise<Array<ResolvedFacetValue>> {
234
+ const query: Record<string, unknown> = { projectId };
235
+ if (searchText) {
236
+ query["name"] = new MultiSearch({
237
+ fields: ["name", "clusterIdentifier"],
238
+ value: searchText,
239
+ });
240
+ }
241
+
242
+ const clusters: Array<KubernetesClusterModel> =
243
+ await KubernetesClusterService.findBy({
244
+ query: query as any,
245
+ select: {
246
+ _id: true,
247
+ name: true,
248
+ clusterIdentifier: true,
249
+ },
250
+ limit: new PositiveNumber(limit),
251
+ skip: new PositiveNumber(0),
252
+ props: { isRoot: true },
253
+ });
254
+
255
+ return ResourceFacetResolver.mergeCounts(
256
+ clusters.map(
257
+ (c: KubernetesClusterModel): { id: string; displayName: string } => {
258
+ return {
259
+ id: c._id ? c._id.toString() : "",
260
+ displayName: c.name || c.clusterIdentifier || "Unknown",
261
+ };
262
+ },
263
+ ),
264
+ counts,
265
+ );
266
+ }
267
+
268
+ /*
269
+ * Combine Postgres-sourced entities with counts from the ClickHouse sample.
270
+ * Entities without a count default to 0 (they exist in the project but
271
+ * had no telemetry in the active window). Sorts active-first so the
272
+ * highest-traffic resources surface at the top of the sidebar.
273
+ */
274
+ private static mergeCounts(
275
+ entities: Array<{ id: string; displayName: string }>,
276
+ counts: Map<string, number>,
277
+ ): Array<ResolvedFacetValue> {
278
+ const out: Array<ResolvedFacetValue> = entities
279
+ .filter((e: { id: string }): boolean => {
280
+ return e.id.length > 0;
281
+ })
282
+ .map((e: { id: string; displayName: string }): ResolvedFacetValue => {
283
+ return {
284
+ value: e.id,
285
+ count: counts.get(e.id) || 0,
286
+ displayName: e.displayName,
287
+ };
288
+ });
289
+
290
+ out.sort((a: ResolvedFacetValue, b: ResolvedFacetValue): number => {
291
+ if (a.count !== b.count) {
292
+ return b.count - a.count;
293
+ }
294
+ return a.displayName.localeCompare(b.displayName);
295
+ });
296
+
297
+ return out;
298
+ }
299
+ }
@@ -52,6 +52,39 @@ import MonitorStepDockerMonitor, {
52
52
  } from "./MonitorStepDockerMonitor";
53
53
  import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
54
54
 
55
+ /*
56
+ * Caps and defaults for per-step request timeout and retry settings.
57
+ * Users may lower these via the UI; values higher than the cap are clamped.
58
+ */
59
+ export const MAX_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000; // 60 seconds
60
+ export const DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000;
61
+ export const MAX_MONITOR_RETRY_COUNT: number = 3;
62
+ export const DEFAULT_MONITOR_RETRY_COUNT: number = 3;
63
+
64
+ export const clampMonitorRequestTimeoutInMs: (value: number) => number = (
65
+ value: number,
66
+ ): number => {
67
+ if (!value || value <= 0) {
68
+ return DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS;
69
+ }
70
+ if (value > MAX_MONITOR_REQUEST_TIMEOUT_IN_MS) {
71
+ return MAX_MONITOR_REQUEST_TIMEOUT_IN_MS;
72
+ }
73
+ return value;
74
+ };
75
+
76
+ export const clampMonitorRetryCount: (value: number) => number = (
77
+ value: number,
78
+ ): number => {
79
+ if (value === undefined || value === null || isNaN(value) || value < 0) {
80
+ return DEFAULT_MONITOR_RETRY_COUNT;
81
+ }
82
+ if (value > MAX_MONITOR_RETRY_COUNT) {
83
+ return MAX_MONITOR_RETRY_COUNT;
84
+ }
85
+ return value;
86
+ };
87
+
55
88
  export interface MonitorStepType {
56
89
  id: string;
57
90
  monitorDestination?: URL | IP | Hostname | undefined;
@@ -88,6 +121,19 @@ export interface MonitorStepType {
88
121
  // retry count for synthetic monitors - number of times to retry on error
89
122
  retryCountOnError?: number | undefined;
90
123
 
124
+ /*
125
+ * Per-step request timeout in milliseconds for probe-based monitors
126
+ * (Website, API, Ping, IP, Port, SSLCertificate). Defaults to and is
127
+ * capped at 60000 ms (60 seconds).
128
+ */
129
+ requestTimeoutInMs?: number | undefined;
130
+
131
+ /*
132
+ * Per-step retry count for probe-based monitors when a check fails.
133
+ * Defaults to and is capped at 3.
134
+ */
135
+ retryCount?: number | undefined;
136
+
91
137
  // Log monitor type.
92
138
  logMonitor?: MonitorStepLogMonitor | undefined;
93
139
 
@@ -148,6 +194,8 @@ export default class MonitorStep extends DatabaseProperty {
148
194
  screenSizeTypes: undefined,
149
195
  browserTypes: undefined,
150
196
  retryCountOnError: undefined,
197
+ requestTimeoutInMs: undefined,
198
+ retryCount: undefined,
151
199
  logMonitor: undefined,
152
200
  traceMonitor: undefined,
153
201
  metricMonitor: undefined,
@@ -190,6 +238,8 @@ export default class MonitorStep extends DatabaseProperty {
190
238
  screenSizeTypes: undefined,
191
239
  browserTypes: undefined,
192
240
  retryCountOnError: undefined,
241
+ requestTimeoutInMs: undefined,
242
+ retryCount: undefined,
193
243
  logMonitor: undefined,
194
244
  traceMonitor: undefined,
195
245
  metricMonitor: undefined,
@@ -294,6 +344,27 @@ export default class MonitorStep extends DatabaseProperty {
294
344
  return this;
295
345
  }
296
346
 
347
+ public setRequestTimeoutInMs(
348
+ requestTimeoutInMs: number | undefined,
349
+ ): MonitorStep {
350
+ if (requestTimeoutInMs === undefined) {
351
+ this.data!.requestTimeoutInMs = undefined;
352
+ return this;
353
+ }
354
+ this.data!.requestTimeoutInMs =
355
+ clampMonitorRequestTimeoutInMs(requestTimeoutInMs);
356
+ return this;
357
+ }
358
+
359
+ public setRetryCount(retryCount: number | undefined): MonitorStep {
360
+ if (retryCount === undefined) {
361
+ this.data!.retryCount = undefined;
362
+ return this;
363
+ }
364
+ this.data!.retryCount = clampMonitorRetryCount(retryCount);
365
+ return this;
366
+ }
367
+
297
368
  public setLogMonitor(logMonitor: MonitorStepLogMonitor): MonitorStep {
298
369
  this.data!.logMonitor = logMonitor;
299
370
  return this;
@@ -400,6 +471,8 @@ export default class MonitorStep extends DatabaseProperty {
400
471
  screenSizeTypes: undefined,
401
472
  browserTypes: undefined,
402
473
  retryCountOnError: undefined,
474
+ requestTimeoutInMs: undefined,
475
+ retryCount: undefined,
403
476
  logMonitor: undefined,
404
477
  exceptionMonitor: undefined,
405
478
  kubernetesMonitor: undefined,
@@ -597,6 +670,11 @@ export default class MonitorStep extends DatabaseProperty {
597
670
  screenSizeTypes: this.data.screenSizeTypes || undefined,
598
671
  browserTypes: this.data.browserTypes || undefined,
599
672
  retryCountOnError: this.data.retryCountOnError || undefined,
673
+ requestTimeoutInMs: this.data.requestTimeoutInMs || undefined,
674
+ retryCount:
675
+ this.data.retryCount === undefined
676
+ ? undefined
677
+ : this.data.retryCount,
600
678
  logMonitor: this.data.logMonitor
601
679
  ? MonitorStepLogMonitorUtil.toJSON(
602
680
  this.data.logMonitor || MonitorStepLogMonitorUtil.getDefault(),
@@ -745,6 +823,11 @@ export default class MonitorStep extends DatabaseProperty {
745
823
  (json["screenSizeTypes"] as Array<ScreenSizeType>) || undefined,
746
824
  browserTypes: (json["browserTypes"] as Array<BrowserType>) || undefined,
747
825
  retryCountOnError: (json["retryCountOnError"] as number) || undefined,
826
+ requestTimeoutInMs: (json["requestTimeoutInMs"] as number) || undefined,
827
+ retryCount:
828
+ json["retryCount"] === undefined || json["retryCount"] === null
829
+ ? undefined
830
+ : (json["retryCount"] as number),
748
831
  logMonitor: json["logMonitor"]
749
832
  ? (json["logMonitor"] as JSONObject)
750
833
  : undefined,
@@ -806,6 +889,8 @@ export default class MonitorStep extends DatabaseProperty {
806
889
  screenSizeTypes: Zod.any().optional(),
807
890
  browserTypes: Zod.any().optional(),
808
891
  retryCountOnError: Zod.number().optional(),
892
+ requestTimeoutInMs: Zod.number().optional(),
893
+ retryCount: Zod.number().optional(),
809
894
  logMonitor: Zod.any().optional(),
810
895
  traceMonitor: Zod.any().optional(),
811
896
  metricMonitor: Zod.any().optional(),
@@ -0,0 +1,6 @@
1
+ enum StatusPageGroupViewMode {
2
+ List = "List",
3
+ Grid = "Grid",
4
+ }
5
+
6
+ export default StatusPageGroupViewMode;
@@ -75,7 +75,9 @@ const Accordion: FunctionComponent<ComponentProps> = (
75
75
  <div className={className}>
76
76
  <div>
77
77
  <div
78
- className={`flex justify-between cursor-pointer`}
78
+ className={`flex justify-between items-start gap-3 cursor-pointer group/accordion-header rounded-lg -mx-2 px-2 py-2 transition-colors ${
79
+ isOpen ? "" : "hover:bg-gray-50/80"
80
+ }`}
79
81
  role="button"
80
82
  tabIndex={0}
81
83
  aria-expanded={isOpen}
@@ -85,43 +87,47 @@ const Accordion: FunctionComponent<ComponentProps> = (
85
87
  }}
86
88
  onKeyDown={handleKeyDown}
87
89
  >
88
- <div className="flex">
90
+ <div className="flex items-start min-w-0 flex-1">
89
91
  {props.title && (
90
- <div>
91
- {isOpen && (
92
- <Icon
93
- className="h-4 w-4 text-gray-500"
94
- icon={IconProp.ChevronDown}
95
- thick={ThickProp.Thick}
96
- />
97
- )}
98
- {!isOpen && (
99
- <Icon
100
- className="h-4 w-4 text-gray-500"
101
- icon={IconProp.ChevronRight}
102
- thick={ThickProp.Thick}
103
- />
104
- )}
92
+ <div
93
+ className={`flex-shrink-0 mt-0.5 flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 ${
94
+ isOpen
95
+ ? "bg-gray-900/5 text-gray-700"
96
+ : "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
97
+ }`}
98
+ aria-hidden="true"
99
+ >
100
+ <Icon
101
+ className={`h-3.5 w-3.5 transition-transform duration-200 ease-out ${
102
+ isOpen ? "rotate-90" : ""
103
+ }`}
104
+ icon={IconProp.ChevronRight}
105
+ thick={ThickProp.Thick}
106
+ />
105
107
  </div>
106
108
  )}
107
109
  {props.title && (
108
110
  <div
109
- className={`ml-1 -mt-1 ${
111
+ className={`ml-2.5 min-w-0 flex-1 ${
110
112
  props.onClick ? "cursor-pointer" : ""
111
113
  }`}
112
114
  >
113
- <div className={`text-gray-500 ${props.titleClassName}`}>
114
- {props.title}{" "}
115
- </div>
116
- <div className="mb-2 text-sm">
117
- {props.description && (
118
- <MarkdownViewer text={props.description || ""} />
119
- )}
115
+ <div
116
+ className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
117
+ >
118
+ {props.title}
120
119
  </div>
120
+ {props.description && (
121
+ <div className="mt-1 text-sm text-gray-500 leading-relaxed">
122
+ <MarkdownViewer text={props.description} />
123
+ </div>
124
+ )}
121
125
  </div>
122
126
  )}
123
127
  </div>
124
- {!isOpen && <div className="">{props.rightElement}</div>}
128
+ {!isOpen && props.rightElement && (
129
+ <div className="flex-shrink-0 mt-0.5">{props.rightElement}</div>
130
+ )}
125
131
  </div>
126
132
  {isOpen && (
127
133
  <div
@@ -89,6 +89,15 @@ export interface ComponentProps {
89
89
  facetLoading?: boolean;
90
90
  onFacetInclude?: (facetKey: string, value: string) => void;
91
91
  onFacetExclude?: (facetKey: string, value: string) => void;
92
+ /*
93
+ * Debounced search emit for resource facets (serviceId / hostId / etc.).
94
+ * Parent updates state and re-fetches facets so the result includes
95
+ * matching resources from the Postgres source-of-truth, not just those
96
+ * already loaded into the sidebar.
97
+ */
98
+ onFacetSearchChange?:
99
+ | ((facetKey: string, searchText: string) => void)
100
+ | undefined;
92
101
  showFacetSidebar?: boolean;
93
102
  activeFilters?: Array<ActiveFilter> | undefined;
94
103
  baseActiveFilters?: Array<ActiveFilter> | undefined;
@@ -1063,6 +1072,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
1063
1072
  savedViews={props.savedViews}
1064
1073
  selectedSavedViewId={props.selectedSavedViewId}
1065
1074
  onSavedViewSelect={props.onSavedViewSelect}
1075
+ onFacetSearchChange={props.onFacetSearchChange}
1066
1076
  />
1067
1077
  )}
1068
1078
 
@@ -1,6 +1,8 @@
1
1
  import React, {
2
2
  FunctionComponent,
3
3
  ReactElement,
4
+ useEffect,
5
+ useRef,
4
6
  useState,
5
7
  useMemo,
6
8
  } from "react";
@@ -19,10 +21,17 @@ export interface FacetSectionProps {
19
21
  valueDisplayMap?: Record<string, string> | undefined;
20
22
  valueColorMap?: Record<string, string> | undefined;
21
23
  activeValues?: Set<string> | undefined;
24
+ /*
25
+ * When set, the search box also emits typed text to the parent (debounced)
26
+ * so it can refetch values from the backend. Client-side filtering still
27
+ * runs as defense-in-depth.
28
+ */
29
+ onSearchChange?: ((text: string) => void) | undefined;
22
30
  }
23
31
 
24
32
  const DEFAULT_VISIBLE_COUNT: number = 5;
25
33
  const SEARCH_THRESHOLD: number = 6;
34
+ const SEARCH_DEBOUNCE_MS: number = 300;
26
35
 
27
36
  const FacetSection: FunctionComponent<FacetSectionProps> = (
28
37
  props: FacetSectionProps,
@@ -31,7 +40,31 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
31
40
  const [showAll, setShowAll] = useState<boolean>(false);
32
41
  const [searchText, setSearchText] = useState<string>("");
33
42
 
34
- const showSearch: boolean = props.values.length >= SEARCH_THRESHOLD;
43
+ const showSearch: boolean =
44
+ props.onSearchChange !== undefined ||
45
+ props.values.length >= SEARCH_THRESHOLD;
46
+
47
+ const onSearchChange: FacetSectionProps["onSearchChange"] =
48
+ props.onSearchChange;
49
+ const lastEmittedRef: React.MutableRefObject<string | null> = useRef<
50
+ string | null
51
+ >(null);
52
+ useEffect(() => {
53
+ if (!onSearchChange) {
54
+ return;
55
+ }
56
+ const trimmed: string = searchText.trim();
57
+ if (lastEmittedRef.current === trimmed) {
58
+ return;
59
+ }
60
+ const handle: ReturnType<typeof setTimeout> = setTimeout(() => {
61
+ lastEmittedRef.current = trimmed;
62
+ onSearchChange(trimmed);
63
+ }, SEARCH_DEBOUNCE_MS);
64
+ return () => {
65
+ clearTimeout(handle);
66
+ };
67
+ }, [searchText, onSearchChange]);
35
68
 
36
69
  const filteredValues: Array<FacetValue> = useMemo(() => {
37
70
  if (!searchText.trim()) {
@@ -40,7 +73,9 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
40
73
  const query: string = searchText.toLowerCase().trim();
41
74
  return props.values.filter((facet: FacetValue) => {
42
75
  const displayName: string =
43
- props.valueDisplayMap?.[facet.value] ?? facet.value;
76
+ facet.displayName ??
77
+ props.valueDisplayMap?.[facet.value] ??
78
+ facet.value;
44
79
  return displayName.toLowerCase().includes(query);
45
80
  });
46
81
  }, [props.values, props.valueDisplayMap, searchText]);
@@ -114,7 +149,9 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
114
149
  <FacetValueRow
115
150
  key={facet.value}
116
151
  value={facet.value}
117
- displayValue={props.valueDisplayMap?.[facet.value]}
152
+ displayValue={
153
+ facet.displayName ?? props.valueDisplayMap?.[facet.value]
154
+ }
118
155
  count={facet.count}
119
156
  maxCount={maxCount}
120
157
  color={props.valueColorMap?.[facet.value]}
@@ -28,8 +28,23 @@ export interface LogsFacetSidebarProps {
28
28
  savedViews?: Array<LogsSavedViewOption> | undefined;
29
29
  selectedSavedViewId?: string | null | undefined;
30
30
  onSavedViewSelect?: ((viewId: string) => void) | undefined;
31
+ /*
32
+ * Called (debounced) when typing in a resource facet's search box. Lets
33
+ * the parent re-issue the facets request with the typed text scoped to
34
+ * that facet, so the result includes resources beyond the loaded subset.
35
+ */
36
+ onFacetSearchChange?:
37
+ | ((facetKey: string, searchText: string) => void)
38
+ | undefined;
31
39
  }
32
40
 
41
+ const RESOURCE_FACET_KEYS: ReadonlySet<string> = new Set([
42
+ "serviceId",
43
+ "hostId",
44
+ "dockerHostId",
45
+ "kubernetesClusterId",
46
+ ]);
47
+
33
48
  const SEVERITY_ORDER: Array<string> = [
34
49
  LogSeverity.Fatal,
35
50
  LogSeverity.Error,
@@ -278,6 +293,13 @@ const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
278
293
  valueColorMap = severityColorMap;
279
294
  }
280
295
 
296
+ const onSearchChange: ((text: string) => void) | undefined =
297
+ RESOURCE_FACET_KEYS.has(key) && props.onFacetSearchChange
298
+ ? (text: string) => {
299
+ props.onFacetSearchChange!(key, text);
300
+ }
301
+ : undefined;
302
+
281
303
  return (
282
304
  <FacetSection
283
305
  key={key}
@@ -289,6 +311,7 @@ const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
289
311
  valueDisplayMap={valueDisplayMap}
290
312
  valueColorMap={valueColorMap}
291
313
  activeValues={activeValuesByKey[key]}
314
+ onSearchChange={onSearchChange}
292
315
  />
293
316
  );
294
317
  })}
@@ -13,6 +13,8 @@ export interface HistogramBucket {
13
13
  export interface FacetValue {
14
14
  value: string;
15
15
  count: number;
16
+ // Server-resolved display name (e.g. serviceId → service name).
17
+ displayName?: string | undefined;
16
18
  }
17
19
 
18
20
  export type FacetData = Record<string, Array<FacetValue>>;