@oneuptime/common 10.0.31 → 10.0.34

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 (133) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +29 -4
  2. package/Models/AnalyticsModels/Log.ts +110 -4
  3. package/Models/AnalyticsModels/Metric.ts +16 -9
  4. package/Models/AnalyticsModels/MonitorLog.ts +4 -2
  5. package/Models/AnalyticsModels/Span.ts +79 -6
  6. package/Models/DatabaseModels/Index.ts +8 -0
  7. package/Models/DatabaseModels/LogDropFilter.ts +480 -0
  8. package/Models/DatabaseModels/LogPipeline.ts +412 -0
  9. package/Models/DatabaseModels/LogPipelineProcessor.ts +430 -0
  10. package/Models/DatabaseModels/LogScrubRule.ts +516 -0
  11. package/Server/API/TelemetryAPI.ts +261 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +79 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +41 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.ts +57 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Middleware/WhatsAppAuthorization.ts +87 -0
  18. package/Server/Services/AnalyticsDatabaseService.ts +61 -0
  19. package/Server/Services/LogAggregationService.ts +238 -1
  20. package/Server/Services/LogDropFilterService.ts +10 -0
  21. package/Server/Services/LogPipelineProcessorService.ts +10 -0
  22. package/Server/Services/LogPipelineService.ts +10 -0
  23. package/Server/Services/LogScrubRuleService.ts +10 -0
  24. package/Server/Services/TelemetryAttributeService.ts +4 -6
  25. package/Server/Utils/AnalyticsDatabase/Statement.ts +15 -1
  26. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +138 -11
  27. package/Tests/Server/Services/LogAggregationService.test.ts +3 -2
  28. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +9 -0
  29. package/Types/AnalyticsDatabase/TableColumnType.ts +4 -0
  30. package/Types/Date.ts +22 -0
  31. package/Types/Log/LogDropFilterAction.ts +6 -0
  32. package/Types/Log/LogPipelineProcessorType.ts +44 -0
  33. package/Types/Log/LogScrubAction.ts +7 -0
  34. package/Types/Log/LogScrubPatternType.ts +10 -0
  35. package/Types/Permission.ts +174 -0
  36. package/UI/Components/LogsViewer/LogsViewer.tsx +152 -4
  37. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +92 -0
  38. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +332 -117
  39. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +294 -274
  40. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +513 -234
  41. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +37 -29
  42. package/UI/Components/LogsViewer/components/LogsTable.tsx +6 -1
  43. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +106 -0
  44. package/UI/Utils/LogExport.ts +160 -0
  45. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +28 -4
  46. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  47. package/build/dist/Models/AnalyticsModels/Log.js +97 -4
  48. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  49. package/build/dist/Models/AnalyticsModels/Metric.js +16 -9
  50. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  51. package/build/dist/Models/AnalyticsModels/MonitorLog.js +4 -2
  52. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  53. package/build/dist/Models/AnalyticsModels/Span.js +73 -6
  54. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  56. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/LogDropFilter.js +508 -0
  58. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -0
  59. package/build/dist/Models/DatabaseModels/LogPipeline.js +438 -0
  60. package/build/dist/Models/DatabaseModels/LogPipeline.js.map +1 -0
  61. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js +452 -0
  62. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js.map +1 -0
  63. package/build/dist/Models/DatabaseModels/LogScrubRule.js +545 -0
  64. package/build/dist/Models/DatabaseModels/LogScrubRule.js.map +1 -0
  65. package/build/dist/Server/API/TelemetryAPI.js +155 -0
  66. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js +52 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js.map +1 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +34 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +1 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +22 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +1 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js +26 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js.map +1 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  77. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +58 -0
  78. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -0
  79. package/build/dist/Server/Services/AnalyticsDatabaseService.js +30 -0
  80. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  81. package/build/dist/Server/Services/LogAggregationService.js +188 -1
  82. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  83. package/build/dist/Server/Services/LogDropFilterService.js +9 -0
  84. package/build/dist/Server/Services/LogDropFilterService.js.map +1 -0
  85. package/build/dist/Server/Services/LogPipelineProcessorService.js +9 -0
  86. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -0
  87. package/build/dist/Server/Services/LogPipelineService.js +9 -0
  88. package/build/dist/Server/Services/LogPipelineService.js.map +1 -0
  89. package/build/dist/Server/Services/LogScrubRuleService.js +9 -0
  90. package/build/dist/Server/Services/LogScrubRuleService.js.map +1 -0
  91. package/build/dist/Server/Services/TelemetryAttributeService.js +4 -6
  92. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  93. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +13 -1
  94. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  95. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +98 -2
  96. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  97. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +3 -2
  98. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  99. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +10 -0
  100. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -0
  101. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js +4 -0
  102. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js.map +1 -1
  103. package/build/dist/Types/Date.js +16 -0
  104. package/build/dist/Types/Date.js.map +1 -1
  105. package/build/dist/Types/Log/LogDropFilterAction.js +7 -0
  106. package/build/dist/Types/Log/LogDropFilterAction.js.map +1 -0
  107. package/build/dist/Types/Log/LogPipelineProcessorType.js +9 -0
  108. package/build/dist/Types/Log/LogPipelineProcessorType.js.map +1 -0
  109. package/build/dist/Types/Log/LogScrubAction.js +8 -0
  110. package/build/dist/Types/Log/LogScrubAction.js.map +1 -0
  111. package/build/dist/Types/Log/LogScrubPatternType.js +11 -0
  112. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -0
  113. package/build/dist/Types/Permission.js +152 -0
  114. package/build/dist/Types/Permission.js.map +1 -1
  115. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +124 -11
  116. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  117. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +36 -0
  118. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -0
  119. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +114 -4
  120. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  121. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +17 -5
  122. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  123. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +229 -122
  124. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  125. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +5 -4
  126. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  127. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +4 -1
  128. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  129. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +28 -0
  130. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  131. package/build/dist/UI/Utils/LogExport.js +129 -0
  132. package/build/dist/UI/Utils/LogExport.js.map +1 -0
  133. package/package.json +1 -1
@@ -14,7 +14,8 @@ import {
14
14
  Tooltip,
15
15
  ResponsiveContainer,
16
16
  CartesianGrid,
17
- Legend,
17
+ AreaChart,
18
+ Area,
18
19
  } from "recharts";
19
20
  import RangeStartAndEndDateTime, {
20
21
  RangeStartAndEndDateTimeUtil,
@@ -57,15 +58,28 @@ export interface LogsAnalyticsViewProps {
57
58
 
58
59
  const CHART_COLORS: Array<string> = [
59
60
  "#6366f1", // indigo
60
- "#f43f5e", // rose
61
+ "#ec4899", // pink
61
62
  "#10b981", // emerald
62
63
  "#f59e0b", // amber
63
64
  "#06b6d4", // cyan
64
- "#ec4899", // pink
65
- "#84cc16", // lime
66
- "#d946ef", // fuchsia
65
+ "#8b5cf6", // violet
66
+ "#f43f5e", // rose
67
+ "#14b8a6", // teal
67
68
  "#64748b", // slate
68
- "#ef4444", // red
69
+ "#84cc16", // lime
70
+ ];
71
+
72
+ const CHART_COLORS_MUTED: Array<string> = [
73
+ "rgba(99,102,241,0.15)",
74
+ "rgba(236,72,153,0.15)",
75
+ "rgba(16,185,129,0.15)",
76
+ "rgba(245,158,11,0.15)",
77
+ "rgba(6,182,212,0.15)",
78
+ "rgba(139,92,246,0.15)",
79
+ "rgba(244,63,94,0.15)",
80
+ "rgba(20,184,166,0.15)",
81
+ "rgba(100,116,139,0.15)",
82
+ "rgba(132,204,22,0.15)",
69
83
  ];
70
84
 
71
85
  const DIMENSION_OPTIONS: Array<{ value: string; label: string }> = [
@@ -162,6 +176,113 @@ function computeDefaultBucketSize(startTime: Date, endTime: Date): number {
162
176
  return 1440;
163
177
  }
164
178
 
179
+ interface AnalyticsTooltipProps {
180
+ active?: boolean;
181
+ label?: string;
182
+ payload?: Array<{
183
+ dataKey: string;
184
+ value: number;
185
+ color: string;
186
+ }>;
187
+ }
188
+
189
+ const AnalyticsTooltip: FunctionComponent<AnalyticsTooltipProps> = (
190
+ props: AnalyticsTooltipProps,
191
+ ): ReactElement | null => {
192
+ if (!props.active || !props.payload || props.payload.length === 0) {
193
+ return null;
194
+ }
195
+
196
+ const entries: Array<{ key: string; value: number; color: string }> =
197
+ props.payload
198
+ .filter((entry: { value: number }): boolean => {
199
+ return entry.value > 0;
200
+ })
201
+ .map(
202
+ (entry: {
203
+ dataKey: string;
204
+ value: number;
205
+ color: string;
206
+ }): { key: string; value: number; color: string } => {
207
+ return {
208
+ key: entry.dataKey,
209
+ value: entry.value,
210
+ color: entry.color,
211
+ };
212
+ },
213
+ );
214
+
215
+ if (entries.length === 0) {
216
+ return null;
217
+ }
218
+
219
+ const total: number = entries.reduce(
220
+ (sum: number, e: { value: number }): number => {
221
+ return sum + e.value;
222
+ },
223
+ 0,
224
+ );
225
+
226
+ const formatTime: (label: string | undefined) => string = (
227
+ label: string | undefined,
228
+ ): string => {
229
+ if (!label) {
230
+ return "";
231
+ }
232
+
233
+ const date: Date = new Date(label);
234
+
235
+ if (isNaN(date.getTime())) {
236
+ return label;
237
+ }
238
+
239
+ return date.toLocaleString([], {
240
+ month: "short",
241
+ day: "numeric",
242
+ hour: "2-digit",
243
+ minute: "2-digit",
244
+ hour12: false,
245
+ });
246
+ };
247
+
248
+ return (
249
+ <div className="rounded-lg border border-gray-200/80 bg-white/95 px-3.5 py-2.5 shadow-lg backdrop-blur-sm">
250
+ <p className="mb-2 border-b border-gray-100 pb-2 font-mono text-[11px] font-medium text-gray-400">
251
+ {formatTime(props.label)}
252
+ </p>
253
+ <div className="space-y-1">
254
+ {entries.map((entry: { key: string; value: number; color: string }) => {
255
+ return (
256
+ <div
257
+ key={entry.key}
258
+ className="flex items-center justify-between gap-8"
259
+ >
260
+ <div className="flex items-center gap-2">
261
+ <span
262
+ className="inline-block h-2.5 w-2.5 rounded-[3px]"
263
+ style={{ backgroundColor: entry.color }}
264
+ />
265
+ <span className="text-xs text-gray-600">{entry.key}</span>
266
+ </div>
267
+ <span className="font-mono text-xs font-semibold tabular-nums text-gray-800">
268
+ {entry.value.toLocaleString()}
269
+ </span>
270
+ </div>
271
+ );
272
+ })}
273
+ </div>
274
+ {entries.length > 1 && (
275
+ <div className="mt-2 flex items-center justify-between border-t border-gray-100 pt-2">
276
+ <span className="text-[11px] font-medium text-gray-400">Total</span>
277
+ <span className="font-mono text-xs font-bold tabular-nums text-gray-900">
278
+ {total.toLocaleString()}
279
+ </span>
280
+ </div>
281
+ )}
282
+ </div>
283
+ );
284
+ };
285
+
165
286
  const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
166
287
  props: LogsAnalyticsViewProps,
167
288
  ): ReactElement => {
@@ -324,154 +445,171 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
324
445
  return pivotTimeseriesData(timeseriesData);
325
446
  }, [timeseriesData]);
326
447
 
448
+ const renderSelectControl: (
449
+ label: string,
450
+ value: string | number,
451
+ onChange: (val: string) => void,
452
+ options: Array<{ value: string | number; label: string }>,
453
+ ) => ReactElement = (
454
+ label: string,
455
+ value: string | number,
456
+ onChange: (val: string) => void,
457
+ options: Array<{ value: string | number; label: string }>,
458
+ ): ReactElement => {
459
+ return (
460
+ <div className="flex items-center">
461
+ <span className="mr-2 text-[11px] font-medium uppercase tracking-wider text-gray-400">
462
+ {label}
463
+ </span>
464
+ <select
465
+ className="appearance-none rounded-md border border-gray-200 bg-white px-2.5 py-1.5 pr-7 text-xs font-medium text-gray-700 shadow-sm transition-all hover:border-gray-300 focus:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-100"
466
+ value={value}
467
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
468
+ onChange(e.target.value);
469
+ }}
470
+ style={{
471
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
472
+ backgroundRepeat: "no-repeat",
473
+ backgroundPosition: "right 6px center",
474
+ }}
475
+ >
476
+ {options.map((opt: { value: string | number; label: string }) => {
477
+ return (
478
+ <option key={opt.value} value={opt.value}>
479
+ {opt.label}
480
+ </option>
481
+ );
482
+ })}
483
+ </select>
484
+ </div>
485
+ );
486
+ };
487
+
327
488
  const renderQueryBuilder: () => ReactElement = (): ReactElement => {
328
489
  return (
329
- <div className="flex flex-wrap items-center gap-3 border-b border-gray-100 bg-gray-50/50 px-4 py-3">
330
- {/* Chart type */}
331
- <div className="flex items-center gap-1.5">
332
- <label className="text-xs font-medium text-gray-500">Chart</label>
333
- <select
334
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
335
- value={chartType}
336
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
337
- setChartType(e.target.value as AnalyticsChartType);
338
- }}
339
- >
340
- <option value="timeseries">Timeseries</option>
341
- <option value="toplist">Top List</option>
342
- <option value="table">Table</option>
343
- </select>
344
- </div>
490
+ <div className="flex flex-wrap items-center gap-4 border-b border-gray-100 px-5 py-3">
491
+ {renderSelectControl(
492
+ "Chart",
493
+ chartType,
494
+ (val: string) => {
495
+ setChartType(val as AnalyticsChartType);
496
+ },
497
+ [
498
+ { value: "timeseries", label: "Timeseries" },
499
+ { value: "toplist", label: "Top List" },
500
+ { value: "table", label: "Table" },
501
+ ],
502
+ )}
345
503
 
346
- {/* Aggregation */}
347
- <div className="flex items-center gap-1.5">
348
- <label className="text-xs font-medium text-gray-500">Measure</label>
349
- <select
350
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
351
- value={aggregation}
352
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
353
- setAggregation(e.target.value as AnalyticsAggregation);
354
- }}
355
- >
356
- <option value="count">Count</option>
357
- <option value="unique">Unique Count</option>
358
- </select>
359
- </div>
504
+ <div className="h-4 w-px bg-gray-200" />
505
+
506
+ {renderSelectControl(
507
+ "Measure",
508
+ aggregation,
509
+ (val: string) => {
510
+ setAggregation(val as AnalyticsAggregation);
511
+ },
512
+ [
513
+ { value: "count", label: "Count" },
514
+ { value: "unique", label: "Unique Count" },
515
+ ],
516
+ )}
360
517
 
361
- {/* Aggregation field for unique count */}
362
518
  {aggregation === "unique" && (
363
- <div className="flex items-center gap-1.5">
364
- <label className="text-xs font-medium text-gray-500">of</label>
365
- <select
366
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
367
- value={aggregationField}
368
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
369
- setAggregationField(e.target.value);
370
- }}
371
- >
372
- <option value="">Select field...</option>
373
- {allDimensionOptions.map(
374
- (opt: { value: string; label: string }) => {
375
- return (
376
- <option key={opt.value} value={opt.value}>
377
- {opt.label}
378
- </option>
379
- );
380
- },
381
- )}
382
- </select>
383
- </div>
519
+ <>
520
+ {renderSelectControl(
521
+ "of",
522
+ aggregationField,
523
+ (val: string) => {
524
+ setAggregationField(val);
525
+ },
526
+ [{ value: "", label: "Select field..." }, ...allDimensionOptions],
527
+ )}
528
+ </>
384
529
  )}
385
530
 
386
- {/* Group by */}
387
- <div className="flex items-center gap-1.5">
388
- <label className="text-xs font-medium text-gray-500">Group by</label>
389
- <select
390
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
391
- value={groupByFields[0] || ""}
392
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
393
- const val: string = e.target.value;
531
+ <div className="h-4 w-px bg-gray-200" />
532
+
533
+ {renderSelectControl(
534
+ "Group by",
535
+ groupByFields[0] || "",
536
+ (val: string) => {
537
+ setGroupByFields((prev: Array<string>) => {
538
+ const next: Array<string> = [...prev];
539
+ next[0] = val;
540
+ return next.filter((f: string) => {
541
+ return f.length > 0;
542
+ });
543
+ });
544
+ },
545
+ [{ value: "", label: "None" }, ...allDimensionOptions],
546
+ )}
547
+
548
+ {groupByFields[0] &&
549
+ groupByFields[0].length > 0 &&
550
+ renderSelectControl(
551
+ "then by",
552
+ groupByFields[1] || "",
553
+ (val: string) => {
394
554
  setGroupByFields((prev: Array<string>) => {
395
- const next: Array<string> = [...prev];
396
- next[0] = val;
555
+ const next: Array<string> = [prev[0] || ""];
556
+
557
+ if (val.length > 0) {
558
+ next.push(val);
559
+ }
560
+
397
561
  return next.filter((f: string) => {
398
562
  return f.length > 0;
399
563
  });
400
564
  });
401
- }}
402
- >
403
- <option value="">None</option>
404
- {allDimensionOptions.map(
405
- (opt: { value: string; label: string }) => {
406
- return (
407
- <option key={opt.value} value={opt.value}>
408
- {opt.label}
409
- </option>
410
- );
565
+ },
566
+ [
567
+ { value: "", label: "None" },
568
+ ...allDimensionOptions.filter((opt: { value: string }) => {
569
+ return opt.value !== groupByFields[0];
570
+ }),
571
+ ],
572
+ )}
573
+
574
+ {(chartType === "toplist" || chartType === "table") && (
575
+ <>
576
+ <div className="h-4 w-px bg-gray-200" />
577
+ {renderSelectControl(
578
+ "Limit",
579
+ topListLimit,
580
+ (val: string) => {
581
+ setTopListLimit(Number(val));
411
582
  },
583
+ TOP_LIST_LIMITS.map((limit: number) => {
584
+ return { value: limit, label: String(limit) };
585
+ }),
412
586
  )}
413
- </select>
414
- </div>
415
-
416
- {/* Second group by (only if first is set) */}
417
- {groupByFields[0] && groupByFields[0].length > 0 && (
418
- <div className="flex items-center gap-1.5">
419
- <label className="text-xs font-medium text-gray-500">then by</label>
420
- <select
421
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
422
- value={groupByFields[1] || ""}
423
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
424
- const val: string = e.target.value;
425
- setGroupByFields((prev: Array<string>) => {
426
- const next: Array<string> = [prev[0] || ""];
427
-
428
- if (val.length > 0) {
429
- next.push(val);
430
- }
431
-
432
- return next.filter((f: string) => {
433
- return f.length > 0;
434
- });
435
- });
436
- }}
437
- >
438
- <option value="">None</option>
439
- {allDimensionOptions
440
- .filter((opt: { value: string }) => {
441
- return opt.value !== groupByFields[0];
442
- })
443
- .map((opt: { value: string; label: string }) => {
444
- return (
445
- <option key={opt.value} value={opt.value}>
446
- {opt.label}
447
- </option>
448
- );
449
- })}
450
- </select>
451
- </div>
587
+ </>
452
588
  )}
589
+ </div>
590
+ );
591
+ };
453
592
 
454
- {/* Limit for top list and table */}
455
- {(chartType === "toplist" || chartType === "table") && (
456
- <div className="flex items-center gap-1.5">
457
- <label className="text-xs font-medium text-gray-500">Limit</label>
458
- <select
459
- className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
460
- value={topListLimit}
461
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
462
- setTopListLimit(Number(e.target.value));
463
- }}
464
- >
465
- {TOP_LIST_LIMITS.map((limit: number) => {
466
- return (
467
- <option key={limit} value={limit}>
468
- {limit}
469
- </option>
470
- );
471
- })}
472
- </select>
473
- </div>
474
- )}
593
+ const renderLegend: () => ReactElement = (): ReactElement => {
594
+ if (seriesKeys.length <= 1) {
595
+ return <></>;
596
+ }
597
+
598
+ return (
599
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 px-5 pb-2">
600
+ {seriesKeys.map((key: string, index: number) => {
601
+ return (
602
+ <div key={key} className="flex items-center gap-1.5">
603
+ <span
604
+ className="inline-block h-2.5 w-2.5 rounded-[3px]"
605
+ style={{
606
+ backgroundColor: CHART_COLORS[index % CHART_COLORS.length],
607
+ }}
608
+ />
609
+ <span className="text-[11px] text-gray-500">{key}</span>
610
+ </div>
611
+ );
612
+ })}
475
613
  </div>
476
614
  );
477
615
  };
@@ -481,79 +619,139 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
481
619
  return renderEmptyState();
482
620
  }
483
621
 
484
- return (
485
- <div className="p-4" style={{ height: 320 }}>
486
- <ResponsiveContainer width="100%" height="100%">
487
- <BarChart
488
- data={pivotedData}
489
- margin={{ top: 8, right: 16, bottom: 0, left: 0 }}
490
- barCategoryGap="15%"
491
- barGap={0}
492
- >
493
- <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
494
- <XAxis
495
- dataKey="time"
496
- tickFormatter={formatTickTime}
497
- tick={{ fontSize: 11, fill: "#9ca3af" }}
498
- axisLine={{ stroke: "#e5e7eb" }}
499
- tickLine={false}
500
- minTickGap={40}
501
- interval="preserveStartEnd"
502
- />
503
- <YAxis
504
- tick={{ fontSize: 11, fill: "#9ca3af" }}
505
- axisLine={false}
506
- tickLine={false}
507
- width={56}
508
- allowDecimals={false}
509
- tickFormatter={formatYAxisTick}
510
- />
511
- <Tooltip
512
- contentStyle={{
513
- fontSize: 12,
514
- borderRadius: 6,
515
- border: "1px solid #e5e7eb",
516
- }}
517
- labelFormatter={(label: string) => {
518
- const d: Date = new Date(label);
519
-
520
- if (isNaN(d.getTime())) {
521
- return label;
522
- }
622
+ const useAreaChart: boolean = seriesKeys.length === 1;
523
623
 
524
- return d.toLocaleString([], {
525
- month: "short",
526
- day: "numeric",
527
- hour: "2-digit",
528
- minute: "2-digit",
529
- hour12: false,
530
- });
531
- }}
532
- />
533
- <Legend
534
- wrapperStyle={{ fontSize: 11 }}
535
- iconType="square"
536
- iconSize={10}
537
- />
538
- {seriesKeys.map((key: string, index: number) => {
539
- return (
540
- <Bar
541
- key={key}
542
- dataKey={key}
543
- stackId="group"
544
- fill={CHART_COLORS[index % CHART_COLORS.length]!}
545
- radius={
546
- index === seriesKeys.length - 1
547
- ? [2, 2, 0, 0]
548
- : [0, 0, 0, 0]
549
- }
624
+ return (
625
+ <div className="px-2 pt-4 pb-2">
626
+ {renderLegend()}
627
+ <div style={{ height: 320 }}>
628
+ <ResponsiveContainer width="100%" height="100%">
629
+ {useAreaChart ? (
630
+ <AreaChart
631
+ data={pivotedData}
632
+ margin={{ top: 8, right: 20, bottom: 4, left: 0 }}
633
+ >
634
+ <defs>
635
+ <linearGradient
636
+ id="areaGradient0"
637
+ x1="0"
638
+ y1="0"
639
+ x2="0"
640
+ y2="1"
641
+ >
642
+ <stop
643
+ offset="0%"
644
+ stopColor={CHART_COLORS[0]}
645
+ stopOpacity={0.2}
646
+ />
647
+ <stop
648
+ offset="95%"
649
+ stopColor={CHART_COLORS[0]}
650
+ stopOpacity={0.02}
651
+ />
652
+ </linearGradient>
653
+ </defs>
654
+ <CartesianGrid
655
+ strokeDasharray="none"
656
+ stroke="#f1f5f9"
657
+ vertical={false}
658
+ />
659
+ <XAxis
660
+ dataKey="time"
661
+ tickFormatter={formatTickTime}
662
+ tick={{ fontSize: 11, fill: "#94a3b8" }}
663
+ axisLine={{ stroke: "#e2e8f0" }}
664
+ tickLine={false}
665
+ minTickGap={50}
666
+ interval="preserveStartEnd"
667
+ dy={8}
668
+ />
669
+ <YAxis
670
+ tick={{ fontSize: 11, fill: "#94a3b8" }}
671
+ axisLine={false}
672
+ tickLine={false}
673
+ width={52}
674
+ allowDecimals={false}
675
+ tickFormatter={formatYAxisTick}
676
+ />
677
+ <Tooltip
678
+ content={<AnalyticsTooltip />}
679
+ cursor={{
680
+ stroke: "#c7d2fe",
681
+ strokeWidth: 1,
682
+ strokeDasharray: "4 4",
683
+ }}
684
+ />
685
+ <Area
686
+ dataKey={seriesKeys[0] || "count"}
687
+ stroke={CHART_COLORS[0]}
688
+ strokeWidth={2}
689
+ fill="url(#areaGradient0)"
690
+ dot={false}
691
+ activeDot={{
692
+ r: 4,
693
+ fill: CHART_COLORS[0],
694
+ stroke: "#fff",
695
+ strokeWidth: 2,
696
+ }}
550
697
  isAnimationActive={false}
551
- maxBarSize={32}
552
698
  />
553
- );
554
- })}
555
- </BarChart>
556
- </ResponsiveContainer>
699
+ </AreaChart>
700
+ ) : (
701
+ <BarChart
702
+ data={pivotedData}
703
+ margin={{ top: 8, right: 20, bottom: 4, left: 0 }}
704
+ barCategoryGap="20%"
705
+ barGap={0}
706
+ >
707
+ <CartesianGrid
708
+ strokeDasharray="none"
709
+ stroke="#f1f5f9"
710
+ vertical={false}
711
+ />
712
+ <XAxis
713
+ dataKey="time"
714
+ tickFormatter={formatTickTime}
715
+ tick={{ fontSize: 11, fill: "#94a3b8" }}
716
+ axisLine={{ stroke: "#e2e8f0" }}
717
+ tickLine={false}
718
+ minTickGap={50}
719
+ interval="preserveStartEnd"
720
+ dy={8}
721
+ />
722
+ <YAxis
723
+ tick={{ fontSize: 11, fill: "#94a3b8" }}
724
+ axisLine={false}
725
+ tickLine={false}
726
+ width={52}
727
+ allowDecimals={false}
728
+ tickFormatter={formatYAxisTick}
729
+ />
730
+ <Tooltip
731
+ content={<AnalyticsTooltip />}
732
+ cursor={{ fill: "rgba(99,102,241,0.04)" }}
733
+ />
734
+ {seriesKeys.map((key: string, index: number) => {
735
+ return (
736
+ <Bar
737
+ key={key}
738
+ dataKey={key}
739
+ stackId="group"
740
+ fill={CHART_COLORS[index % CHART_COLORS.length]!}
741
+ radius={
742
+ index === seriesKeys.length - 1
743
+ ? [3, 3, 0, 0]
744
+ : [0, 0, 0, 0]
745
+ }
746
+ isAnimationActive={false}
747
+ maxBarSize={40}
748
+ />
749
+ );
750
+ })}
751
+ </BarChart>
752
+ )}
753
+ </ResponsiveContainer>
754
+ </div>
557
755
  </div>
558
756
  );
559
757
  };
@@ -570,31 +768,61 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
570
768
  1,
571
769
  );
572
770
 
771
+ const totalCount: number = topListData.reduce(
772
+ (sum: number, item: AnalyticsTopItem) => {
773
+ return sum + item.count;
774
+ },
775
+ 0,
776
+ );
777
+
573
778
  return (
574
- <div className="p-4">
575
- <div className="space-y-2">
779
+ <div className="p-5">
780
+ <div className="space-y-1.5">
576
781
  {topListData.map((item: AnalyticsTopItem, index: number) => {
577
782
  const percentage: number = (item.count / maxCount) * 100;
783
+ const sharePercent: number =
784
+ totalCount > 0 ? Math.round((item.count / totalCount) * 100) : 0;
785
+ const color: string =
786
+ CHART_COLORS[index % CHART_COLORS.length] || CHART_COLORS[0]!;
787
+ const mutedColor: string =
788
+ CHART_COLORS_MUTED[index % CHART_COLORS_MUTED.length] ||
789
+ CHART_COLORS_MUTED[0]!;
790
+
578
791
  return (
579
- <div key={index} className="flex items-center gap-3">
580
- <div className="w-40 truncate text-xs font-medium text-gray-700">
792
+ <div
793
+ key={index}
794
+ className="group flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-gray-50/80"
795
+ >
796
+ <span className="w-5 text-right font-mono text-[11px] font-medium text-gray-300">
797
+ {index + 1}
798
+ </span>
799
+ <div
800
+ className="h-2 w-2 rounded-full flex-shrink-0"
801
+ style={{ backgroundColor: color }}
802
+ />
803
+ <div className="w-44 min-w-0 truncate text-sm font-medium text-gray-700">
581
804
  {item.value || "(empty)"}
582
805
  </div>
583
806
  <div className="flex-1">
584
- <div className="relative h-6 w-full overflow-hidden rounded bg-gray-100">
807
+ <div className="relative h-7 w-full overflow-hidden rounded-md bg-gray-50">
585
808
  <div
586
- className="absolute left-0 top-0 h-full rounded transition-all"
809
+ className="absolute left-0 top-0 h-full rounded-md transition-all duration-300"
587
810
  style={{
588
811
  width: `${percentage}%`,
589
- backgroundColor:
590
- CHART_COLORS[index % CHART_COLORS.length],
812
+ backgroundColor: mutedColor,
813
+ borderLeft: `3px solid ${color}`,
591
814
  }}
592
815
  />
593
- <div className="absolute right-2 top-0 flex h-full items-center text-xs font-medium text-gray-600">
594
- {item.count.toLocaleString()}
595
- </div>
596
816
  </div>
597
817
  </div>
818
+ <div className="flex items-center gap-3 flex-shrink-0">
819
+ <span className="font-mono text-sm font-semibold tabular-nums text-gray-800">
820
+ {item.count.toLocaleString()}
821
+ </span>
822
+ <span className="w-10 text-right font-mono text-[11px] tabular-nums text-gray-400">
823
+ {sharePercent}%
824
+ </span>
825
+ </div>
598
826
  </div>
599
827
  );
600
828
  })}
@@ -612,44 +840,80 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
612
840
  tableData[0]?.groupValues || {},
613
841
  );
614
842
 
843
+ const maxCount: number = Math.max(
844
+ ...tableData.map((row: AnalyticsTableRow) => {
845
+ return row.count;
846
+ }),
847
+ 1,
848
+ );
849
+
615
850
  return (
616
- <div className="p-4">
617
- <div className="overflow-hidden rounded-lg border border-gray-200">
618
- <table className="min-w-full divide-y divide-gray-200">
619
- <thead className="bg-gray-50">
620
- <tr>
851
+ <div className="p-5">
852
+ <div className="overflow-hidden rounded-lg border border-gray-200/80">
853
+ <table className="min-w-full">
854
+ <thead>
855
+ <tr className="border-b border-gray-200/80 bg-gray-50/80">
856
+ <th className="w-10 px-4 py-2.5 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-400">
857
+ #
858
+ </th>
621
859
  {groupKeys.map((key: string) => {
622
860
  return (
623
861
  <th
624
862
  key={key}
625
- className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
863
+ className="px-4 py-2.5 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-400"
626
864
  >
627
865
  {key}
628
866
  </th>
629
867
  );
630
868
  })}
631
- <th className="px-4 py-2 text-right text-xs font-medium uppercase tracking-wider text-gray-500">
869
+ <th className="px-4 py-2.5 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-400">
632
870
  Count
633
871
  </th>
872
+ <th className="w-48 px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wider text-gray-400" />
634
873
  </tr>
635
874
  </thead>
636
- <tbody className="divide-y divide-gray-100 bg-white">
875
+ <tbody>
637
876
  {tableData.map((row: AnalyticsTableRow, index: number) => {
877
+ const barWidth: number = (row.count / maxCount) * 100;
878
+ const color: string =
879
+ CHART_COLORS[index % CHART_COLORS.length] || CHART_COLORS[0]!;
880
+ const mutedColor: string =
881
+ CHART_COLORS_MUTED[index % CHART_COLORS_MUTED.length] ||
882
+ CHART_COLORS_MUTED[0]!;
883
+
638
884
  return (
639
- <tr key={index} className="hover:bg-gray-50">
885
+ <tr
886
+ key={index}
887
+ className="border-b border-gray-100/80 transition-colors last:border-b-0 hover:bg-gray-50/50"
888
+ >
889
+ <td className="px-4 py-2.5 font-mono text-[11px] text-gray-300">
890
+ {index + 1}
891
+ </td>
640
892
  {groupKeys.map((key: string) => {
641
893
  return (
642
894
  <td
643
895
  key={key}
644
- className="whitespace-nowrap px-4 py-2 text-xs text-gray-700"
896
+ className="whitespace-nowrap px-4 py-2.5 text-sm text-gray-700"
645
897
  >
646
898
  {row.groupValues[key] || "(empty)"}
647
899
  </td>
648
900
  );
649
901
  })}
650
- <td className="whitespace-nowrap px-4 py-2 text-right text-xs font-medium text-gray-900">
902
+ <td className="whitespace-nowrap px-4 py-2.5 text-right font-mono text-sm font-semibold tabular-nums text-gray-800">
651
903
  {row.count.toLocaleString()}
652
904
  </td>
905
+ <td className="px-4 py-2.5">
906
+ <div className="h-5 w-full overflow-hidden rounded bg-gray-50">
907
+ <div
908
+ className="h-full rounded transition-all duration-300"
909
+ style={{
910
+ width: `${barWidth}%`,
911
+ backgroundColor: mutedColor,
912
+ borderLeft: `2px solid ${color}`,
913
+ }}
914
+ />
915
+ </div>
916
+ </td>
653
917
  </tr>
654
918
  );
655
919
  })}
@@ -662,8 +926,23 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
662
926
 
663
927
  const renderEmptyState: () => ReactElement = (): ReactElement => {
664
928
  return (
665
- <div className="flex h-64 items-center justify-center text-sm text-gray-400">
666
- No data available for the selected query.
929
+ <div className="flex h-72 flex-col items-center justify-center gap-3">
930
+ <svg
931
+ className="h-10 w-10 text-gray-200"
932
+ fill="none"
933
+ viewBox="0 0 24 24"
934
+ stroke="currentColor"
935
+ strokeWidth={1.5}
936
+ >
937
+ <path
938
+ strokeLinecap="round"
939
+ strokeLinejoin="round"
940
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
941
+ />
942
+ </svg>
943
+ <p className="text-sm text-gray-400">
944
+ No data available for the selected query
945
+ </p>
667
946
  </div>
668
947
  );
669
948
  };
@@ -671,7 +950,7 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
671
950
  const renderChart: () => ReactElement = (): ReactElement => {
672
951
  if (isLoading) {
673
952
  return (
674
- <div className="flex h-64 items-center justify-center">
953
+ <div className="flex h-72 items-center justify-center">
675
954
  <ComponentLoader />
676
955
  </div>
677
956
  );
@@ -689,7 +968,7 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
689
968
  };
690
969
 
691
970
  return (
692
- <div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
971
+ <div className="overflow-hidden rounded-lg border border-gray-200/80 bg-white shadow-sm">
693
972
  {renderQueryBuilder()}
694
973
  {renderChart()}
695
974
  </div>