@oneuptime/common 10.0.85 → 10.0.88

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 (90) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/TelemetryAPI.ts +24 -0
  6. package/Server/EnvironmentConfig.ts +10 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  9. package/Server/Infrastructure/Queue.ts +4 -4
  10. package/Server/Services/TelemetryAttributeService.ts +37 -3
  11. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  12. package/Tests/Types/Date.test.ts +46 -0
  13. package/Types/Date.ts +9 -4
  14. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  15. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  16. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  17. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  18. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  19. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  20. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  21. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  22. package/UI/Components/Filters/Types/Filter.ts +3 -0
  23. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  24. package/UI/Components/Forms/Types/Field.ts +5 -0
  25. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  26. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  27. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  28. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  29. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  30. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  31. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  32. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  33. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  34. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  35. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  36. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  37. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  38. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  39. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  40. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  41. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  42. package/build/dist/Server/EnvironmentConfig.js +3 -0
  43. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  48. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  49. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  50. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  51. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  52. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  53. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  54. package/build/dist/Tests/Types/Date.test.js +40 -0
  55. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  56. package/build/dist/Types/Date.js +7 -2
  57. package/build/dist/Types/Date.js.map +1 -1
  58. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  59. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  60. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  61. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  62. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  63. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  64. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  65. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  66. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  67. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  68. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  69. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  70. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  71. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  72. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  73. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  74. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  75. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  76. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  77. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  78. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  79. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  80. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  81. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  82. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  83. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  84. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  85. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  86. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  87. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  88. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  89. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  90. package/package.json +1 -1
@@ -15,6 +15,7 @@ import AnalyticsTableColumn, {
15
15
  SkipIndexType,
16
16
  } from "../../../Types/AnalyticsDatabase/TableColumn";
17
17
  import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
18
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
18
19
  import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
19
20
  import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
20
21
  import InBetween from "../../../Types/BaseDatabase/InBetween";
@@ -25,7 +26,11 @@ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
25
26
  import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
26
27
  import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
27
28
  import NotEqual from "../../../Types/BaseDatabase/NotEqual";
29
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
30
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
28
31
  import Search from "../../../Types/BaseDatabase/Search";
32
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
33
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
29
34
  import SortOrder from "../../../Types/BaseDatabase/SortOrder";
30
35
  import OneUptimeDate from "../../../Types/Date";
31
36
  import BadDataException from "../../../Types/Exception/BadDataException";
@@ -447,33 +452,195 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
447
452
  tableColumn.type === TableColumnType.MapStringString &&
448
453
  typeof value === "object"
449
454
  ) {
450
- const mapValue: Record<string, string | Search<string>> =
451
- value as Record<string, string | Search<string>>;
455
+ const mapValue: Record<string, unknown> = value as Record<
456
+ string,
457
+ unknown
458
+ >;
452
459
  for (const mapKey in mapValue) {
453
- if (mapValue[mapKey] === undefined) {
460
+ const mapEntry: unknown = mapValue[mapKey];
461
+ if (mapEntry === undefined || mapEntry === null) {
454
462
  continue;
455
463
  }
456
- if (mapValue[mapKey] instanceof Search) {
464
+
465
+ /*
466
+ * ClickHouse Map columns return the value type's default for
467
+ * missing keys (empty string for String values), so to express
468
+ * "is empty" we have to cover both the missing-key and the
469
+ * empty-string case explicitly.
470
+ */
471
+ if (mapEntry instanceof IsNull) {
472
+ whereStatement.append(
473
+ SQL`AND ((NOT mapContains(${key}, ${{
474
+ value: mapKey,
475
+ type: TableColumnType.Text,
476
+ }})) OR ${key}[${{
477
+ value: mapKey,
478
+ type: TableColumnType.Text,
479
+ }}] = '')`,
480
+ );
481
+ continue;
482
+ }
483
+
484
+ if (mapEntry instanceof NotNull) {
485
+ whereStatement.append(
486
+ SQL`AND mapContains(${key}, ${{
487
+ value: mapKey,
488
+ type: TableColumnType.Text,
489
+ }}) AND ${key}[${{
490
+ value: mapKey,
491
+ type: TableColumnType.Text,
492
+ }}] != ''`,
493
+ );
494
+ continue;
495
+ }
496
+
497
+ if (mapEntry instanceof Search) {
498
+ whereStatement.append(
499
+ SQL`AND ${key}[${{
500
+ value: mapKey,
501
+ type: TableColumnType.Text,
502
+ }}] ILIKE ${{
503
+ value: mapEntry as Search<string>,
504
+ type: TableColumnType.Text,
505
+ }}`,
506
+ );
507
+ continue;
508
+ }
509
+
510
+ if (mapEntry instanceof NotContains) {
511
+ const literalValue: string = `%${(mapEntry.value as string) || ""}%`;
512
+ whereStatement.append(
513
+ SQL`AND ${key}[${{
514
+ value: mapKey,
515
+ type: TableColumnType.Text,
516
+ }}] NOT ILIKE ${{
517
+ value: literalValue,
518
+ type: TableColumnType.Text,
519
+ }}`,
520
+ );
521
+ continue;
522
+ }
523
+
524
+ if (mapEntry instanceof StartsWith) {
525
+ const literalValue: string = `${(mapEntry.value as string) || ""}%`;
526
+ whereStatement.append(
527
+ SQL`AND ${key}[${{
528
+ value: mapKey,
529
+ type: TableColumnType.Text,
530
+ }}] ILIKE ${{
531
+ value: literalValue,
532
+ type: TableColumnType.Text,
533
+ }}`,
534
+ );
535
+ continue;
536
+ }
537
+
538
+ if (mapEntry instanceof EndsWith) {
539
+ const literalValue: string = `%${(mapEntry.value as string) || ""}`;
457
540
  whereStatement.append(
458
541
  SQL`AND ${key}[${{
459
542
  value: mapKey,
460
543
  type: TableColumnType.Text,
461
544
  }}] ILIKE ${{
462
- value: mapValue[mapKey] as Search<string>,
545
+ value: literalValue,
463
546
  type: TableColumnType.Text,
464
547
  }}`,
465
548
  );
466
- } else {
549
+ continue;
550
+ }
551
+
552
+ if (mapEntry instanceof NotEqual) {
553
+ whereStatement.append(
554
+ SQL`AND ${key}[${{
555
+ value: mapKey,
556
+ type: TableColumnType.Text,
557
+ }}] != ${{
558
+ value: String((mapEntry as NotEqual<string>).value ?? ""),
559
+ type: TableColumnType.Text,
560
+ }}`,
561
+ );
562
+ continue;
563
+ }
564
+
565
+ if (mapEntry instanceof EqualTo) {
467
566
  whereStatement.append(
468
567
  SQL`AND ${key}[${{
469
568
  value: mapKey,
470
569
  type: TableColumnType.Text,
471
570
  }}] = ${{
472
- value: mapValue[mapKey] as string,
571
+ value: String((mapEntry as EqualTo<any>).value ?? ""),
473
572
  type: TableColumnType.Text,
474
573
  }}`,
475
574
  );
575
+ continue;
476
576
  }
577
+
578
+ /*
579
+ * Map values are stored as text; cast to Float64 for numeric
580
+ * comparisons and skip rows where the cast fails (non-numeric).
581
+ */
582
+ if (mapEntry instanceof GreaterThan) {
583
+ whereStatement.append(
584
+ SQL`AND toFloat64OrNull(${key}[${{
585
+ value: mapKey,
586
+ type: TableColumnType.Text,
587
+ }}]) > ${{
588
+ value: Number((mapEntry as GreaterThan<any>).value),
589
+ type: TableColumnType.Number,
590
+ }}`,
591
+ );
592
+ continue;
593
+ }
594
+
595
+ if (mapEntry instanceof GreaterThanOrEqual) {
596
+ whereStatement.append(
597
+ SQL`AND toFloat64OrNull(${key}[${{
598
+ value: mapKey,
599
+ type: TableColumnType.Text,
600
+ }}]) >= ${{
601
+ value: Number((mapEntry as GreaterThanOrEqual<any>).value),
602
+ type: TableColumnType.Number,
603
+ }}`,
604
+ );
605
+ continue;
606
+ }
607
+
608
+ if (mapEntry instanceof LessThan) {
609
+ whereStatement.append(
610
+ SQL`AND toFloat64OrNull(${key}[${{
611
+ value: mapKey,
612
+ type: TableColumnType.Text,
613
+ }}]) < ${{
614
+ value: Number((mapEntry as LessThan<any>).value),
615
+ type: TableColumnType.Number,
616
+ }}`,
617
+ );
618
+ continue;
619
+ }
620
+
621
+ if (mapEntry instanceof LessThanOrEqual) {
622
+ whereStatement.append(
623
+ SQL`AND toFloat64OrNull(${key}[${{
624
+ value: mapKey,
625
+ type: TableColumnType.Text,
626
+ }}]) <= ${{
627
+ value: Number((mapEntry as LessThanOrEqual<any>).value),
628
+ type: TableColumnType.Number,
629
+ }}`,
630
+ );
631
+ continue;
632
+ }
633
+
634
+ // Bare string/number/boolean — back-compat with existing data.
635
+ whereStatement.append(
636
+ SQL`AND ${key}[${{
637
+ value: mapKey,
638
+ type: TableColumnType.Text,
639
+ }}] = ${{
640
+ value: String(mapEntry),
641
+ type: TableColumnType.Text,
642
+ }}`,
643
+ );
477
644
  }
478
645
  } else if (
479
646
  (tableColumn.type === TableColumnType.JSON ||
@@ -250,4 +250,50 @@ describe("class OneUptimeDate", () => {
250
250
  expect(result[0]).not.toContain("EST");
251
251
  });
252
252
  });
253
+
254
+ describe("getDateAsFormattedArrayInMultipleTimezones default timezones", () => {
255
+ test("defaults include UTC, London, New York, LA, Kolkata, Sydney", () => {
256
+ const result: Array<string> =
257
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
258
+ date: new Date("2026-07-15T17:00:00Z"),
259
+ use12HourFormat: true,
260
+ });
261
+ expect(result).toHaveLength(6);
262
+ expect(result[0]).toContain("UTC");
263
+ });
264
+
265
+ test("summer (DST) event shows EDT and BST in defaults, not EST or GMT", () => {
266
+ /*
267
+ * Apr 27 2026 21:30 UTC — both US and UK are in DST. Use word
268
+ * boundaries so AEST/AEDT (Sydney) doesn't match EST/EDT.
269
+ */
270
+ const result: Array<string> =
271
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
272
+ date: new Date("2026-04-27T21:30:00Z"),
273
+ use12HourFormat: true,
274
+ });
275
+ const joined: string = result.join("\n");
276
+ expect(joined).toMatch(/\bEDT\b/);
277
+ expect(joined).toMatch(/\bBST\b/);
278
+ expect(joined).not.toMatch(/\bEST\b/);
279
+ expect(joined).not.toMatch(/\bGMT\b/);
280
+ });
281
+
282
+ test("winter (no DST) event shows EST and GMT in defaults, not EDT or BST", () => {
283
+ /*
284
+ * Jan 15 2026 17:00 UTC — both US and UK are on standard time. Use
285
+ * word boundaries so AEDT (Sydney) doesn't match EDT.
286
+ */
287
+ const result: Array<string> =
288
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
289
+ date: new Date("2026-01-15T17:00:00Z"),
290
+ use12HourFormat: true,
291
+ });
292
+ const joined: string = result.join("\n");
293
+ expect(joined).toMatch(/\bEST\b/);
294
+ expect(joined).toMatch(/\bGMT\b/);
295
+ expect(joined).not.toMatch(/\bEDT\b/);
296
+ expect(joined).not.toMatch(/\bBST\b/);
297
+ });
298
+ });
253
299
  });
package/Types/Date.ts CHANGED
@@ -1385,13 +1385,18 @@ export default class OneUptimeDate {
1385
1385
  formatstring = "MMM DD, YYYY";
1386
1386
  }
1387
1387
 
1388
- // convert this date into GMT, EST, PST, IST, ACT with moment
1389
1388
  const timezoneDates: Array<string> = [];
1390
1389
 
1391
1390
  if (!timezones || timezones.length === 0) {
1391
+ /*
1392
+ * Use IANA region zones (not fixed-offset zones like "EST") so the
1393
+ * abbreviation reflects DST at the event's date — e.g. EST/EDT for
1394
+ * America/New_York and GMT/BST for Europe/London.
1395
+ */
1392
1396
  timezones = [
1393
1397
  Timezone.UTC,
1394
- Timezone.EST,
1398
+ Timezone.EuropeLondon,
1399
+ Timezone.AmericaNew_York,
1395
1400
  Timezone.AmericaLos_Angeles,
1396
1401
  Timezone.AsiaKolkata,
1397
1402
  Timezone.AustraliaSydney,
@@ -1416,7 +1421,7 @@ export default class OneUptimeDate {
1416
1421
  public static getDateAsFormattedHTMLInMultipleTimezones(data: {
1417
1422
  date: string | Date;
1418
1423
  onlyShowDate?: boolean;
1419
- timezones?: Array<Timezone> | undefined; // if this is skipped, then it will show the default timezones in the order of UTC, EST, PST, IST, ACT
1424
+ timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
1420
1425
  use12HourFormat?: boolean | undefined;
1421
1426
  }): string {
1422
1427
  const date: string | Date = data.date;
@@ -1435,7 +1440,7 @@ export default class OneUptimeDate {
1435
1440
  public static getDateAsFormattedStringInMultipleTimezones(data: {
1436
1441
  date: string | Date;
1437
1442
  onlyShowDate?: boolean | undefined;
1438
- timezones?: Array<Timezone> | undefined; // if this is skipped, then it will show the default timezones in the order of UTC, EST, PST, IST, ACT
1443
+ timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
1439
1444
  use12HourFormat?: boolean | undefined;
1440
1445
  }): string {
1441
1446
  const date: string | Date = data.date;
@@ -21,6 +21,9 @@ export interface ComponentProps {
21
21
  onBlur?: (() => void) | undefined;
22
22
  outerDivClassName?: string | undefined;
23
23
  disableSpellCheck?: boolean | undefined;
24
+ isLoadingSuggestions?: boolean | undefined;
25
+ loadingMessage?: string | undefined;
26
+ noSuggestionsMessage?: string | undefined;
24
27
  }
25
28
 
26
29
  const BASE_INPUT_CLASS: string =
@@ -90,7 +93,9 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
90
93
  .slice(0, MAX_SUGGESTIONS);
91
94
  }, [inputValue, props.suggestions]);
92
95
 
93
- const showMenu: boolean = isMenuVisible && suggestions.length > 0;
96
+ const isLoadingSuggestions: boolean = Boolean(props.isLoadingSuggestions);
97
+ const showMenu: boolean =
98
+ isMenuVisible && (suggestions.length > 0 || isLoadingSuggestions);
94
99
 
95
100
  const getInputClassName: () => string = (): string => {
96
101
  let className: string = props.className || BASE_INPUT_CLASS;
@@ -145,7 +150,7 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
145
150
  const handleKeyDown: (
146
151
  event: React.KeyboardEvent<HTMLInputElement>,
147
152
  ) => void = (event: React.KeyboardEvent<HTMLInputElement>) => {
148
- if (!showMenu) {
153
+ if (!showMenu || suggestions.length === 0) {
149
154
  return;
150
155
  }
151
156
 
@@ -221,26 +226,60 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
221
226
  id={listboxIdRef.current}
222
227
  role="listbox"
223
228
  >
224
- {suggestions.map((suggestion: string, index: number) => {
225
- const isActive: boolean = index === highlightedIndex;
226
- return (
227
- <button
228
- key={`${suggestion}-${index}`}
229
- type="button"
230
- role="option"
231
- aria-selected={isActive}
232
- className={`flex w-full items-center px-3 py-2 text-left hover:bg-indigo-50 ${isActive ? "bg-indigo-600 text-white hover:bg-indigo-500" : "text-gray-700"}`}
233
- onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
234
- event.preventDefault();
235
- }}
236
- onClick={() => {
237
- handleSuggestionSelect(suggestion);
238
- }}
229
+ {isLoadingSuggestions && (
230
+ <div className="flex w-full items-center px-3 py-2 text-left text-gray-500">
231
+ <svg
232
+ className="animate-spin -ml-0.5 mr-2 h-4 w-4 text-indigo-500"
233
+ xmlns="http://www.w3.org/2000/svg"
234
+ fill="none"
235
+ viewBox="0 0 24 24"
236
+ aria-hidden="true"
239
237
  >
240
- {suggestion}
241
- </button>
242
- );
243
- })}
238
+ <circle
239
+ className="opacity-25"
240
+ cx="12"
241
+ cy="12"
242
+ r="10"
243
+ stroke="currentColor"
244
+ strokeWidth="4"
245
+ ></circle>
246
+ <path
247
+ className="opacity-75"
248
+ fill="currentColor"
249
+ d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
250
+ ></path>
251
+ </svg>
252
+ <span>{props.loadingMessage || "Loading..."}</span>
253
+ </div>
254
+ )}
255
+ {!isLoadingSuggestions &&
256
+ suggestions.length === 0 &&
257
+ props.noSuggestionsMessage && (
258
+ <div className="flex w-full items-center px-3 py-2 text-left text-gray-500">
259
+ {props.noSuggestionsMessage}
260
+ </div>
261
+ )}
262
+ {!isLoadingSuggestions &&
263
+ suggestions.map((suggestion: string, index: number) => {
264
+ const isActive: boolean = index === highlightedIndex;
265
+ return (
266
+ <button
267
+ key={`${suggestion}-${index}`}
268
+ type="button"
269
+ role="option"
270
+ aria-selected={isActive}
271
+ className={`flex w-full items-center px-3 py-2 text-left hover:bg-indigo-50 ${isActive ? "bg-indigo-600 text-white hover:bg-indigo-500" : "text-gray-700"}`}
272
+ onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
273
+ event.preventDefault();
274
+ }}
275
+ onClick={() => {
276
+ handleSuggestionSelect(suggestion);
277
+ }}
278
+ >
279
+ {suggestion}
280
+ </button>
281
+ );
282
+ })}
244
283
  </div>
245
284
  )}
246
285
  </div>