@datum-cloud/datum-ui 3.7.1 → 3.10.0

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 (106) hide show
  1. package/dist/app-navigation/index.mjs +1 -1
  2. package/dist/{app-navigation-Bcx7hr9L.mjs → app-navigation-DyNZwlSG.mjs} +4 -4
  3. package/dist/assistant/index.mjs +9 -8
  4. package/dist/autocomplete/index.mjs +1 -1
  5. package/dist/{autocomplete-fSSmRLYK.mjs → autocomplete-CrCbO2VS.mjs} +12 -6
  6. package/dist/autosearch/index.mjs +3 -3
  7. package/dist/badge/index.mjs +182 -1
  8. package/dist/button/index.mjs +2 -2
  9. package/dist/{button-Dj6cKrWj.mjs → button-Cn4KagBO.mjs} +1 -1
  10. package/dist/{button-BU8TEbJ5.mjs → button-IK-Lnu_9.mjs} +1 -1
  11. package/dist/button-group/index.mjs +1 -1
  12. package/dist/{button-group-C3GKo-0p.mjs → button-group-DJ6X1SSj.mjs} +2 -2
  13. package/dist/{calendar-date-picker-BUwgVaI1.mjs → calendar-date-picker-BRKs56gR.mjs} +1 -1
  14. package/dist/card/index.mjs +1 -1
  15. package/dist/{card-save-bar-0XDpAJo_.mjs → card-save-bar-Te8jf-ie.mjs} +1 -1
  16. package/dist/checkbox/index.mjs +1 -1
  17. package/dist/checkbox-BAo5sbHk.mjs +84 -0
  18. package/dist/collapsible/index.mjs +1 -6
  19. package/dist/collapsible-B-KP3JZ-.mjs +104 -0
  20. package/dist/components/base/checkbox/checkbox.d.ts +11 -3
  21. package/dist/components/base/collapsible/collapsible.d.ts +28 -0
  22. package/dist/components/base/collapsible/index.d.ts +1 -1
  23. package/dist/components/base/radio-group/radio-group.d.ts +11 -5
  24. package/dist/components/base/spinner/index.d.ts +1 -1
  25. package/dist/components/base/spinner/spinner.d.ts +11 -0
  26. package/dist/components/base/switch/switch.d.ts +6 -1
  27. package/dist/components/base/typography/typography.d.ts +1 -1
  28. package/dist/components/features/_shared/copy-state-icon.d.ts +12 -0
  29. package/dist/components/features/_shared/index.d.ts +2 -0
  30. package/dist/components/features/_shared/motion-chip.d.ts +293 -0
  31. package/dist/components/features/logs/hooks/use-logs-url-state.d.ts +59 -0
  32. package/dist/components/features/logs/index.d.ts +4 -1
  33. package/dist/components/features/logs/utils/facet-counts.d.ts +50 -0
  34. package/dist/components/features/logs/utils/facets.d.ts +2 -0
  35. package/dist/components/features/logs/utils/index.d.ts +4 -0
  36. package/dist/components/features/logs/utils/url-state.d.ts +61 -0
  37. package/dist/components/icons/spinner.icon.d.ts +17 -3
  38. package/dist/copy-state-icon-DMvXXsZm.mjs +61 -0
  39. package/dist/data-table/index.mjs +1 -1
  40. package/dist/{data-table-s2YrE79S.mjs → data-table-DFUnUydN.mjs} +8 -8
  41. package/dist/date-picker/index.mjs +3 -3
  42. package/dist/date-time-picker/index.mjs +1 -1
  43. package/dist/{date-time-picker-CLdaGiGC.mjs → date-time-picker-BRMm2QSz.mjs} +1 -1
  44. package/dist/{date-time-range-picker-RZX4CS4b.mjs → date-time-range-picker-CqGGJTLS.mjs} +20 -9
  45. package/dist/dropzone/index.mjs +1 -1
  46. package/dist/{dropzone-Cer9E-VH.mjs → dropzone-qKYfSe7o.mjs} +1 -1
  47. package/dist/empty-content/index.mjs +1 -1
  48. package/dist/{empty-content-B2FZ9cyt.mjs → empty-content-CTMHak36.mjs} +1 -1
  49. package/dist/field-styles-D-FJatHt.mjs +31 -0
  50. package/dist/form/index.mjs +1 -1
  51. package/dist/form/stepper/index.mjs +1 -1
  52. package/dist/{form-C4XcxxjK.mjs → form-VdbSqDjL.mjs} +20 -13
  53. package/dist/grouped-table/index.mjs +1 -1
  54. package/dist/{grouped-table-32RO2v7U.mjs → grouped-table-B4HzLXq6.mjs} +3 -3
  55. package/dist/hooks/index.mjs +1 -1
  56. package/dist/hooks/use-breakpoint.d.ts +7 -2
  57. package/dist/icons/index.mjs +1 -1
  58. package/dist/index.mjs +37 -37
  59. package/dist/{layers-XEND2WKv.mjs → layers-BC1Ny49w.mjs} +11 -3
  60. package/dist/loader-overlay/index.mjs +1 -1
  61. package/dist/{loader-overlay-JvB8IQo3.mjs → loader-overlay-DP0pvHuG.mjs} +1 -1
  62. package/dist/logs/index.mjs +412 -115
  63. package/dist/map/index.mjs +1 -1
  64. package/dist/more-actions/index.mjs +1 -1
  65. package/dist/{more-actions-D9k1ILzT.mjs → more-actions-DXQiV-bN.mjs} +2 -2
  66. package/dist/motion-chip-BczV7spP.mjs +52 -0
  67. package/dist/multi-select/index.mjs +1 -1
  68. package/dist/{multi-select-BUgx6VZF.mjs → multi-select-DwcsJdHt.mjs} +82 -66
  69. package/dist/picker/index.mjs +2 -2
  70. package/dist/{picker-C0h30DHf.mjs → picker-BHPtncpv.mjs} +2 -2
  71. package/dist/radio-group/index.mjs +1 -1
  72. package/dist/radio-group-Boxi2hm0.mjs +83 -0
  73. package/dist/responsive-dropdown/index.mjs +1 -1
  74. package/dist/{responsive-dropdown-DsWgpE_i.mjs → responsive-dropdown-B9cDUeHt.mjs} +1 -1
  75. package/dist/responsive-popover/index.mjs +1 -1
  76. package/dist/{responsive-popover-DJYd8lOK.mjs → responsive-popover-B_pUxrZ1.mjs} +1 -1
  77. package/dist/rich-text-editor/index.mjs +1 -1
  78. package/dist/{rich-text-editor-ibngjhQV.mjs → rich-text-editor-CPd_6vdZ.mjs} +2 -2
  79. package/dist/select/index.mjs +1 -1
  80. package/dist/{select-DemYhwnG.mjs → select-B0Xlz-wD.mjs} +3 -2
  81. package/dist/{select-jgK6wyXj.mjs → select-CGsHf70u.mjs} +7 -2
  82. package/dist/spinner/index.mjs +1 -1
  83. package/dist/spinner-DSlDJdWl.mjs +23 -0
  84. package/dist/spinner.icon-BiUnMAxw.mjs +104 -0
  85. package/dist/switch/index.mjs +1 -1
  86. package/dist/switch-IduVwiPZ.mjs +114 -0
  87. package/dist/tabs/index.mjs +1 -1
  88. package/dist/tag-input/index.mjs +1 -1
  89. package/dist/{tag-input-DAMY703F.mjs → tag-input-a3ktkGrH.mjs} +39 -32
  90. package/dist/task-queue/index.mjs +1 -1
  91. package/dist/{task-queue-dropdown-BgQSGDwg.mjs → task-queue-dropdown-BflEFiMi.mjs} +4 -4
  92. package/dist/time-picker/index.mjs +1 -1
  93. package/dist/{time-picker-DgHjdcMZ.mjs → time-picker-BFwH-Vd3.mjs} +1 -1
  94. package/dist/{to-api-format-EZInfj0_.mjs → to-api-format-BiWOTFha.mjs} +4 -4
  95. package/dist/transfer/index.mjs +1 -1
  96. package/dist/{transfer-91lwTOhX.mjs → transfer-DGUeAQUL.mjs} +1 -1
  97. package/dist/{use-breakpoint-zY_JYINK.mjs → use-breakpoint-Ce5tS4cU.mjs} +11 -5
  98. package/dist/{use-option-picker-CBMU72LU.mjs → use-option-picker-DXY2SNnT.mjs} +2 -2
  99. package/dist/utils/field-styles.d.ts +21 -0
  100. package/package.json +1 -4
  101. package/dist/badge-D7A6GfWn.mjs +0 -182
  102. package/dist/checkbox-CNljVnKj.mjs +0 -28
  103. package/dist/radio-group-0RxTmRnF.mjs +0 -43
  104. package/dist/spinner-DjEpx6Q3.mjs +0 -13
  105. package/dist/spinner.icon-D0SKuGFG.mjs +0 -47
  106. package/dist/switch-CWvVN9ea.mjs +0 -33
@@ -1,19 +1,20 @@
1
1
  import { t as cn } from "../utils-CTIuhCOs.mjs";
2
2
  import { Skeleton } from "../skeleton/index.mjs";
3
- import { t as Badge } from "../badge-D7A6GfWn.mjs";
4
- import { t as useBreakpoint } from "../use-breakpoint-zY_JYINK.mjs";
3
+ import { Badge } from "../badge/index.mjs";
4
+ import { t as useBreakpoint } from "../use-breakpoint-Ce5tS4cU.mjs";
5
5
  import { t as useCopyToClipboard } from "../use-copy-to-clipboard-CWKWHLxI.mjs";
6
- import { t as Button } from "../button-Dj6cKrWj.mjs";
6
+ import { t as Button } from "../button-Cn4KagBO.mjs";
7
7
  import { t as Sheet } from "../sheet-DEpEJ_Rk.mjs";
8
8
  import { t as Icon } from "../icon-wrapper-CIk2th6R.mjs";
9
+ import { t as CopyStateIcon } from "../copy-state-icon-DMvXXsZm.mjs";
9
10
  import { c as getBrowserTimezone, t as DateTime, u as getTimezoneAbbreviation } from "../date-time-BBRVM9qb.mjs";
10
- import { t as Checkbox } from "../checkbox-CNljVnKj.mjs";
11
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../collapsible/index.mjs";
12
- import { t as DateTimeRangePicker } from "../date-time-range-picker-RZX4CS4b.mjs";
11
+ import { t as Checkbox } from "../checkbox-BAo5sbHk.mjs";
12
+ import { n as CollapsibleContent, r as CollapsibleTrigger, t as Collapsible } from "../collapsible-B-KP3JZ-.mjs";
13
+ import { t as DateTimeRangePicker } from "../date-time-range-picker-CqGGJTLS.mjs";
13
14
  import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/index.mjs";
14
15
  import { t as Input } from "../input-DsT5qkJn.mjs";
15
- import { Bot, ChevronDown, ChevronUp, Clock, Copy, Download, HelpCircle, Inbox, Monitor, RefreshCw, Search, SlidersHorizontal, Smartphone, Tablet, TriangleAlert, X } from "lucide-react";
16
- import { createContext, memo, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
16
+ import { Bot, ChevronDown, ChevronUp, Clock, Download, HelpCircle, Inbox, Monitor, RefreshCw, Search, SlidersHorizontal, Smartphone, Tablet, TriangleAlert, X } from "lucide-react";
17
+ import { createContext, memo, use, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
17
18
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
19
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
19
20
  import { format, isSameDay } from "date-fns";
@@ -319,6 +320,322 @@ function useLogs() {
319
320
  if (!context) throw new Error("Logs components must be used within <Logs.Root>");
320
321
  return context;
321
322
  }
323
+ // src/components/features/logs/utils/time-range.ts
324
+ const MINUTE_MS$1 = 6e4;
325
+ const HOUR_MS = 60 * MINUTE_MS$1;
326
+ const DAY_MS$1 = 24 * HOUR_MS;
327
+ function relativePreset(key, label, durationMs, shortcut) {
328
+ return {
329
+ key,
330
+ label,
331
+ shortcut,
332
+ getRange: () => {
333
+ const now = /* @__PURE__ */ new Date();
334
+ return {
335
+ from: new Date(now.getTime() - durationMs),
336
+ to: now
337
+ };
338
+ }
339
+ };
340
+ }
341
+ /**
342
+ * Relative "Last N" windows offered by `Logs.Filters`. Keys are persisted on
343
+ * `LogTimeRange.preset` so hosts can slide the window forward with
344
+ * `resolveLogTimeRange` on refresh or while live tailing.
345
+ *
346
+ * Shortcuts follow the `picker` datetime presets (`1` / `2` / `H` / `W`) that
347
+ * the logs picker is built on. Keys that mean something else in the pickers
348
+ * (`3` is "Last 30 minutes" in `time-range-picker`, `D` is "Today") are not
349
+ * reused, so muscle memory carries over between the two.
350
+ */
351
+ const LOG_TIME_PRESETS = [
352
+ relativePreset("last-15m", "Last 15 minutes", 15 * MINUTE_MS$1, "1"),
353
+ relativePreset("last-30m", "Last 30 minutes", 30 * MINUTE_MS$1, "2"),
354
+ relativePreset("last-1h", "Last hour", HOUR_MS, "H"),
355
+ relativePreset("last-3h", "Last 3 hours", 3 * HOUR_MS),
356
+ relativePreset("last-6h", "Last 6 hours", 6 * HOUR_MS, "6"),
357
+ relativePreset("last-12h", "Last 12 hours", 12 * HOUR_MS),
358
+ relativePreset("last-24h", "Last 24 hours", DAY_MS$1),
359
+ relativePreset("last-7d", "Last 7 days", 7 * DAY_MS$1, "W")
360
+ ];
361
+ function lastThirtyMinutes(now = /* @__PURE__ */ new Date()) {
362
+ return {
363
+ from: (/* @__PURE__ */ new Date(now.getTime() - 30 * MINUTE_MS$1)).toISOString(),
364
+ to: now.toISOString(),
365
+ preset: "last-30m"
366
+ };
367
+ }
368
+ /**
369
+ * Re-anchor a preset range to `now`. Absolute ranges (no `preset`, or an
370
+ * unknown key) are returned unchanged.
371
+ */
372
+ function resolveLogTimeRange(range, now = /* @__PURE__ */ new Date()) {
373
+ if (!range.preset) return range;
374
+ const preset = LOG_TIME_PRESETS.find((item) => item.key === range.preset);
375
+ if (!preset) return range;
376
+ const { from, to } = preset.getRange("UTC");
377
+ const shift = now.getTime() - to.getTime();
378
+ return {
379
+ from: new Date(from.getTime() + shift).toISOString(),
380
+ to: now.toISOString(),
381
+ preset: range.preset
382
+ };
383
+ }
384
+ /** Human label for the time-range trigger: preset name, or a compact absolute range. */
385
+ function logTimeRangeLabel(range) {
386
+ const preset = range.preset ? LOG_TIME_PRESETS.find((item) => item.key === range.preset) : void 0;
387
+ if (preset) return preset.label;
388
+ const from = new Date(range.from);
389
+ const to = new Date(range.to);
390
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return "Select range";
391
+ if (isSameDay(from, to)) return `${format(from, "MMM d, HH:mm")} – ${format(to, "HH:mm")}`;
392
+ return `${format(from, "MMM d, HH:mm")} – ${format(to, "MMM d, HH:mm")}`;
393
+ }
394
+ function filtersAreActive(filters) {
395
+ return Object.values(filters).some((values) => values.length > 0);
396
+ }
397
+ // src/components/features/logs/utils/url-state.ts
398
+ /**
399
+ * Query-string keys, before `prefix`. A preset range is written as
400
+ * `range=last-1h`; a hand-picked one as `from` and `to` ISO timestamps. Each
401
+ * selected facet value is its own `filter=<label>:<value>` entry.
402
+ */
403
+ const LOGS_URL_KEYS = {
404
+ search: "q",
405
+ range: "range",
406
+ from: "from",
407
+ to: "to",
408
+ filter: "filter",
409
+ live: "live",
410
+ selected: "log"
411
+ };
412
+ /** Same rule as `buildLogQL`: anything else can't be a Loki label. */
413
+ const LABEL_NAME$1 = /^[a-z_]\w*$/i;
414
+ function keysFor(prefix = "") {
415
+ return {
416
+ search: `${prefix}${LOGS_URL_KEYS.search}`,
417
+ range: `${prefix}${LOGS_URL_KEYS.range}`,
418
+ from: `${prefix}${LOGS_URL_KEYS.from}`,
419
+ to: `${prefix}${LOGS_URL_KEYS.to}`,
420
+ filter: `${prefix}${LOGS_URL_KEYS.filter}`,
421
+ live: `${prefix}${LOGS_URL_KEYS.live}`,
422
+ selected: `${prefix}${LOGS_URL_KEYS.selected}`
423
+ };
424
+ }
425
+ function toParams(input) {
426
+ return typeof input === "string" ? new URLSearchParams(input) : input;
427
+ }
428
+ function isValidDate(value) {
429
+ return value !== null && value !== "" && !Number.isNaN(new Date(value).getTime());
430
+ }
431
+ function isKnownPreset(key) {
432
+ return LOG_TIME_PRESETS.some((preset) => preset.key === key);
433
+ }
434
+ function sameRange(a, b) {
435
+ if (a.preset || b.preset) return a.preset === b.preset;
436
+ return a.from === b.from && a.to === b.to;
437
+ }
438
+ function parseTimeRange(params, keys, fallback, now) {
439
+ const preset = params.get(keys.range);
440
+ if (preset && isKnownPreset(preset)) return resolveLogTimeRange({
441
+ from: "",
442
+ to: "",
443
+ preset
444
+ }, now);
445
+ const from = params.get(keys.from);
446
+ const to = params.get(keys.to);
447
+ if (isValidDate(from) && isValidDate(to) && new Date(from) < new Date(to)) return {
448
+ from: new Date(from).toISOString(),
449
+ to: new Date(to).toISOString()
450
+ };
451
+ return fallback.preset ? resolveLogTimeRange(fallback, now) : fallback;
452
+ }
453
+ function parseFilters(params, key) {
454
+ const filters = {};
455
+ for (const entry of params.getAll(key)) {
456
+ const split = entry.indexOf(":");
457
+ if (split <= 0) continue;
458
+ const name = entry.slice(0, split);
459
+ const value = entry.slice(split + 1);
460
+ if (!LABEL_NAME$1.test(name) || value === "") continue;
461
+ const values = filters[name] ?? (filters[name] = []);
462
+ if (!values.includes(value)) values.push(value);
463
+ }
464
+ return filters;
465
+ }
466
+ /**
467
+ * Read logs state from a query string. Missing or malformed values fall back
468
+ * to the defaults (`Logs.Root`'s own defaults), so a hand-edited or stale link
469
+ * still opens. A preset range is resolved against `now`.
470
+ */
471
+ function parseLogsUrlState(input, options = {}) {
472
+ const params = toParams(input);
473
+ const keys = keysFor(options.prefix);
474
+ const now = options.now ?? /* @__PURE__ */ new Date();
475
+ const fallback = options.defaultTimeRange ?? lastThirtyMinutes(now);
476
+ const live = params.get(keys.live);
477
+ return {
478
+ timeRange: parseTimeRange(params, keys, fallback, now),
479
+ filters: parseFilters(params, keys.filter),
480
+ search: params.get(keys.search) ?? "",
481
+ live: live === "1" || live === "true",
482
+ selectedId: params.get(keys.selected) || null
483
+ };
484
+ }
485
+ /**
486
+ * Return a copy of `input` with the given parts of logs state written in.
487
+ * Only the parts present in `state` are touched, and params the app owns are
488
+ * kept. Defaults (no search, no filters, not live, the default range) are
489
+ * left out so a fresh explorer has a clean URL.
490
+ */
491
+ function writeLogsUrlState(input, state, options = {}) {
492
+ const params = new URLSearchParams(toParams(input));
493
+ const keys = keysFor(options.prefix);
494
+ if (state.timeRange !== void 0) {
495
+ params.delete(keys.range);
496
+ params.delete(keys.from);
497
+ params.delete(keys.to);
498
+ const range = state.timeRange;
499
+ if (!sameRange(range, options.defaultTimeRange ?? {
500
+ from: "",
501
+ to: "",
502
+ preset: "last-30m"
503
+ })) {
504
+ if (range.preset && isKnownPreset(range.preset)) params.set(keys.range, range.preset);
505
+ else if (isValidDate(range.from) && isValidDate(range.to)) {
506
+ params.set(keys.from, range.from);
507
+ params.set(keys.to, range.to);
508
+ }
509
+ }
510
+ }
511
+ if (state.filters !== void 0) {
512
+ params.delete(keys.filter);
513
+ for (const [name, values] of Object.entries(state.filters)) {
514
+ if (!LABEL_NAME$1.test(name)) continue;
515
+ for (const value of values) if (value !== "") params.append(keys.filter, `${name}:${value}`);
516
+ }
517
+ }
518
+ if (state.search !== void 0) {
519
+ if (state.search === "") params.delete(keys.search);
520
+ else params.set(keys.search, state.search);
521
+ }
522
+ if (state.live !== void 0) {
523
+ if (state.live) params.set(keys.live, "1");
524
+ else params.delete(keys.live);
525
+ }
526
+ if (state.selectedId !== void 0) {
527
+ if (state.selectedId) params.set(keys.selected, state.selectedId);
528
+ else params.delete(keys.selected);
529
+ }
530
+ return params;
531
+ }
532
+ /**
533
+ * Just this explorer's params, as a query string in a fixed key order. It
534
+ * parses to the same state as the full query string, and only changes when
535
+ * one of these params does, so it works as a memo key: an unrelated app param
536
+ * changing won't re-resolve a preset range (which would hand the host a new
537
+ * `timeRange` and trigger a refetch).
538
+ */
539
+ function logsUrlStateKey(input, options = {}) {
540
+ const params = toParams(input);
541
+ const own = new URLSearchParams();
542
+ for (const key of Object.values(keysFor(options.prefix))) for (const value of params.getAll(key)) own.append(key, value);
543
+ return own.toString();
544
+ }
545
+ // src/components/features/logs/hooks/use-logs-url-state.ts
546
+ /** pushState/replaceState don't fire `popstate`, so writes announce themselves. */
547
+ const LOCATION_CHANGE_EVENT = "datum-ui:logs-url-change";
548
+ function subscribeToLocation(onChange) {
549
+ window.addEventListener("popstate", onChange);
550
+ window.addEventListener(LOCATION_CHANGE_EVENT, onChange);
551
+ return () => {
552
+ window.removeEventListener("popstate", onChange);
553
+ window.removeEventListener(LOCATION_CHANGE_EVENT, onChange);
554
+ };
555
+ }
556
+ const getLocationSearch = () => window.location.search;
557
+ const getServerSearch = () => "";
558
+ const setWindowSearchParams = (update, options) => {
559
+ const next = update(new URLSearchParams(window.location.search)).toString();
560
+ const url = `${window.location.pathname}${next ? `?${next}` : ""}${window.location.hash}`;
561
+ if (options?.replace === false) window.history.pushState(window.history.state, "", url);
562
+ else window.history.replaceState(window.history.state, "", url);
563
+ window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
564
+ };
565
+ /**
566
+ * Keep `Logs.Root`'s time range, filters, search, live toggle and open row in
567
+ * the address bar, so a refresh or a shared link reopens the same view.
568
+ *
569
+ * ```tsx
570
+ * const logsState = useLogsUrlState()
571
+ * const query = buildLogQL({ matchers: logsState.filters, lineContains: logsState.search })
572
+ * // fetch with query + resolveLogTimeRange(logsState.timeRange)
573
+ *
574
+ * <Logs.Root {...logsState} entries={entries} onRefresh={refetch}>
575
+ * <Logs.Explorer />
576
+ * </Logs.Root>
577
+ * ```
578
+ *
579
+ * With React Router, hand it the router's search params instead:
580
+ *
581
+ * ```tsx
582
+ * const [searchParams, setSearchParams] = useSearchParams()
583
+ * const logsState = useLogsUrlState({ searchParams, setSearchParams })
584
+ * ```
585
+ */
586
+ function useLogsUrlState(options = {}) {
587
+ const { searchParams, setSearchParams, prefix = "", defaultTimeRange, history = "replace" } = options;
588
+ const locationSearch = useSyncExternalStore(subscribeToLocation, getLocationSearch, getServerSearch);
589
+ const write = setSearchParams ?? setWindowSearchParams;
590
+ const replace = history !== "push";
591
+ const hasDefault = defaultTimeRange !== void 0;
592
+ const defaultFrom = defaultTimeRange?.from;
593
+ const defaultTo = defaultTimeRange?.to;
594
+ const defaultPreset = defaultTimeRange?.preset;
595
+ const urlOptions = useMemo(() => ({
596
+ prefix,
597
+ defaultTimeRange: hasDefault ? {
598
+ from: defaultFrom ?? "",
599
+ to: defaultTo ?? "",
600
+ preset: defaultPreset
601
+ } : void 0
602
+ }), [
603
+ prefix,
604
+ hasDefault,
605
+ defaultFrom,
606
+ defaultTo,
607
+ defaultPreset
608
+ ]);
609
+ const ownQuery = logsUrlStateKey(searchParams ?? locationSearch, { prefix });
610
+ const state = useMemo(() => parseLogsUrlState(ownQuery, urlOptions), [ownQuery, urlOptions]);
611
+ const update = useCallback((patch) => {
612
+ write((prev) => writeLogsUrlState(prev, patch, urlOptions), { replace });
613
+ }, [
614
+ write,
615
+ urlOptions,
616
+ replace
617
+ ]);
618
+ const onTimeRangeChange = useCallback((timeRange) => update({ timeRange }), [update]);
619
+ const onFiltersChange = useCallback((filters) => update({ filters }), [update]);
620
+ const onSearchChange = useCallback((search) => update({ search }), [update]);
621
+ const onLiveChange = useCallback((live) => update({ live }), [update]);
622
+ const onSelectedIdChange = useCallback((selectedId) => update({ selectedId }), [update]);
623
+ return useMemo(() => ({
624
+ ...state,
625
+ onTimeRangeChange,
626
+ onFiltersChange,
627
+ onSearchChange,
628
+ onLiveChange,
629
+ onSelectedIdChange
630
+ }), [
631
+ state,
632
+ onTimeRangeChange,
633
+ onFiltersChange,
634
+ onSearchChange,
635
+ onLiveChange,
636
+ onSelectedIdChange
637
+ ]);
638
+ }
322
639
  function clampLogsDetailWidth(width, maxWidth) {
323
640
  return Math.min(Math.max(Math.round(width), 400), maxWidth > 400 ? maxWidth : 400);
324
641
  }
@@ -688,9 +1005,29 @@ function keyedParams(params) {
688
1005
  };
689
1006
  });
690
1007
  }
1008
+ /**
1009
+ * Icon button that copies `text` and shows a check until the copied state
1010
+ * resets. Each button owns its hook so only the one clicked flips.
1011
+ */
1012
+ function LogsCopyButton({ text, label, small = false }) {
1013
+ const [copied, copy] = useCopyToClipboard();
1014
+ return /* @__PURE__ */ jsx(Button, {
1015
+ type: "secondary",
1016
+ theme: "borderless",
1017
+ size: "icon",
1018
+ className: small ? "-my-1 size-6" : void 0,
1019
+ "aria-label": label,
1020
+ onClick: () => copy(text, { withToast: true }),
1021
+ children: /* @__PURE__ */ jsx(CopyStateIcon, {
1022
+ copied,
1023
+ size: small ? 14 : 16,
1024
+ strokeWidth: 1,
1025
+ absoluteStrokeWidth: true
1026
+ })
1027
+ });
1028
+ }
691
1029
  const DetailBody = memo(({ showClose = true }) => {
692
1030
  const { selectedEntry, selectPrevious, selectNext, setSelectedId } = useLogs();
693
- const [, copy] = useCopyToClipboard();
694
1031
  if (!selectedEntry) return null;
695
1032
  const parsed = parseLogLine(selectedEntry.line, selectedEntry.labels);
696
1033
  const displayLine = parsed.kind === "http" && !selectedEntry.line.trim() ? formatHttpLogLine(parsed) : selectedEntry.line;
@@ -729,13 +1066,9 @@ const DetailBody = memo(({ showClose = true }) => {
729
1066
  onClick: selectNext,
730
1067
  children: /* @__PURE__ */ jsx(Icon, { icon: ChevronDown })
731
1068
  }),
732
- /* @__PURE__ */ jsx(Button, {
733
- type: "secondary",
734
- theme: "borderless",
735
- size: "icon",
736
- "aria-label": "Copy log line",
737
- onClick: () => copy(displayLine, { withToast: true }),
738
- children: /* @__PURE__ */ jsx(Icon, { icon: Copy })
1069
+ /* @__PURE__ */ jsx(LogsCopyButton, {
1070
+ text: displayLine,
1071
+ label: "Copy log line"
739
1072
  }),
740
1073
  showClose && /* @__PURE__ */ jsx(Button, {
741
1074
  type: "secondary",
@@ -827,17 +1160,10 @@ const DetailBody = memo(({ showClose = true }) => {
827
1160
  children: [/* @__PURE__ */ jsx("span", {
828
1161
  className: "text-muted-foreground text-3xs font-medium",
829
1162
  children: "Client"
830
- }), /* @__PURE__ */ jsx(Button, {
831
- type: "secondary",
832
- theme: "borderless",
833
- size: "icon",
834
- className: "-my-1 size-6",
835
- "aria-label": "Copy user agent",
836
- onClick: () => copy(client.raw, { withToast: true }),
837
- children: /* @__PURE__ */ jsx(Icon, {
838
- icon: Copy,
839
- size: 14
840
- })
1163
+ }), /* @__PURE__ */ jsx(LogsCopyButton, {
1164
+ text: client.raw,
1165
+ label: "Copy user agent",
1166
+ small: true
841
1167
  })]
842
1168
  }),
843
1169
  /* @__PURE__ */ jsxs("div", {
@@ -873,17 +1199,10 @@ const DetailBody = memo(({ showClose = true }) => {
873
1199
  children: [/* @__PURE__ */ jsx("span", {
874
1200
  className: "text-muted-foreground text-3xs font-medium",
875
1201
  children: "Search params"
876
- }), /* @__PURE__ */ jsx(Button, {
877
- type: "secondary",
878
- theme: "borderless",
879
- size: "icon",
880
- className: "-my-1 size-6",
881
- "aria-label": "Copy search params",
882
- onClick: () => copy(`?${request.search}`, { withToast: true }),
883
- children: /* @__PURE__ */ jsx(Icon, {
884
- icon: Copy,
885
- size: 14
886
- })
1202
+ }), /* @__PURE__ */ jsx(LogsCopyButton, {
1203
+ text: `?${request.search}`,
1204
+ label: "Copy search params",
1205
+ small: true
887
1206
  })]
888
1207
  }), /* @__PURE__ */ jsx("dl", {
889
1208
  className: "flex flex-wrap gap-1.5",
@@ -1027,84 +1346,15 @@ function LogsDetail({ className }) {
1027
1346
  }), /* @__PURE__ */ jsx(DetailBody, {})]
1028
1347
  }) });
1029
1348
  }
1030
- // src/components/features/logs/utils/time-range.ts
1031
- const MINUTE_MS$1 = 6e4;
1032
- const HOUR_MS = 60 * MINUTE_MS$1;
1033
- const DAY_MS$1 = 24 * HOUR_MS;
1034
- function relativePreset(key, label, durationMs, shortcut) {
1035
- return {
1036
- key,
1037
- label,
1038
- shortcut,
1039
- getRange: () => {
1040
- const now = /* @__PURE__ */ new Date();
1041
- return {
1042
- from: new Date(now.getTime() - durationMs),
1043
- to: now
1044
- };
1045
- }
1046
- };
1047
- }
1048
- /**
1049
- * Relative "Last N" windows offered by `Logs.Filters`. Keys are persisted on
1050
- * `LogTimeRange.preset` so hosts can slide the window forward with
1051
- * `resolveLogTimeRange` on refresh or while live tailing.
1052
- *
1053
- * Shortcuts follow the `picker` datetime presets (`1` / `2` / `H` / `W`) that
1054
- * the logs picker is built on. Keys that mean something else in the pickers
1055
- * (`3` is "Last 30 minutes" in `time-range-picker`, `D` is "Today") are not
1056
- * reused, so muscle memory carries over between the two.
1057
- */
1058
- const LOG_TIME_PRESETS = [
1059
- relativePreset("last-15m", "Last 15 minutes", 15 * MINUTE_MS$1, "1"),
1060
- relativePreset("last-30m", "Last 30 minutes", 30 * MINUTE_MS$1, "2"),
1061
- relativePreset("last-1h", "Last hour", HOUR_MS, "H"),
1062
- relativePreset("last-3h", "Last 3 hours", 3 * HOUR_MS),
1063
- relativePreset("last-6h", "Last 6 hours", 6 * HOUR_MS, "6"),
1064
- relativePreset("last-12h", "Last 12 hours", 12 * HOUR_MS),
1065
- relativePreset("last-24h", "Last 24 hours", DAY_MS$1),
1066
- relativePreset("last-7d", "Last 7 days", 7 * DAY_MS$1, "W")
1067
- ];
1068
- function lastThirtyMinutes(now = /* @__PURE__ */ new Date()) {
1069
- return {
1070
- from: (/* @__PURE__ */ new Date(now.getTime() - 30 * MINUTE_MS$1)).toISOString(),
1071
- to: now.toISOString(),
1072
- preset: "last-30m"
1073
- };
1074
- }
1075
- /**
1076
- * Re-anchor a preset range to `now`. Absolute ranges (no `preset`, or an
1077
- * unknown key) are returned unchanged.
1078
- */
1079
- function resolveLogTimeRange(range, now = /* @__PURE__ */ new Date()) {
1080
- if (!range.preset) return range;
1081
- const preset = LOG_TIME_PRESETS.find((item) => item.key === range.preset);
1082
- if (!preset) return range;
1083
- const { from, to } = preset.getRange("UTC");
1084
- const shift = now.getTime() - to.getTime();
1085
- return {
1086
- from: new Date(from.getTime() + shift).toISOString(),
1087
- to: now.toISOString(),
1088
- preset: range.preset
1089
- };
1090
- }
1091
- /** Human label for the time-range trigger: preset name, or a compact absolute range. */
1092
- function logTimeRangeLabel(range) {
1093
- const preset = range.preset ? LOG_TIME_PRESETS.find((item) => item.key === range.preset) : void 0;
1094
- if (preset) return preset.label;
1095
- const from = new Date(range.from);
1096
- const to = new Date(range.to);
1097
- if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return "Select range";
1098
- if (isSameDay(from, to)) return `${format(from, "MMM d, HH:mm")} – ${format(to, "HH:mm")}`;
1099
- return `${format(from, "MMM d, HH:mm")} – ${format(to, "MMM d, HH:mm")}`;
1100
- }
1101
- function filtersAreActive(filters) {
1102
- return Object.values(filters).some((values) => values.length > 0);
1103
- }
1104
1349
  // src/components/features/logs/components/filters.tsx
1105
1350
  function LogsFilterGroup({ facet, defaultOpen }) {
1106
1351
  const { filters, toggleFilterValue } = useLogs();
1107
1352
  const selected = filters[facet.name] ?? [];
1353
+ const known = new Set(facet.options.map((option) => option.value));
1354
+ const options = [...facet.options, ...selected.filter((value) => !known.has(value)).map((value) => ({
1355
+ value,
1356
+ count: void 0
1357
+ }))];
1108
1358
  return /* @__PURE__ */ jsxs(Collapsible, {
1109
1359
  defaultOpen,
1110
1360
  className: "border-b border-border",
@@ -1116,7 +1366,7 @@ function LogsFilterGroup({ facet, defaultOpen }) {
1116
1366
  })]
1117
1367
  }), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("ul", {
1118
1368
  className: "flex flex-col gap-1 px-3 pb-3",
1119
- children: facet.options.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
1369
+ children: options.map((option) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
1120
1370
  className: "hover:bg-muted/40 flex cursor-pointer items-center gap-2 rounded-md px-1 py-1 text-sm",
1121
1371
  children: [
1122
1372
  /* @__PURE__ */ jsx(Checkbox, {
@@ -1593,7 +1843,7 @@ function LogsSearch({ placeholder = "Search logs...", className }) {
1593
1843
  onChange: (event) => setValue(event.target.value),
1594
1844
  placeholder,
1595
1845
  "aria-label": placeholder,
1596
- className: "pl-9",
1846
+ className: "h-9 pl-9",
1597
1847
  "data-slot": "logs-search"
1598
1848
  })]
1599
1849
  });
@@ -1981,6 +2231,53 @@ function buildLogQL({ matchers, lineContains } = {}) {
1981
2231
  if (!text) return selector;
1982
2232
  return `${selector} |= "${escapeQuoted(text)}"`;
1983
2233
  }
2234
+ // src/components/features/logs/utils/facet-counts.ts
2235
+ /** Same rule as `buildLogQL`: anything else can't be a Loki label. */
2236
+ const LABEL_NAME = /^[a-z_]\w*$/i;
2237
+ /** Whole seconds between `from` and `to`, at least 1. */
2238
+ function rangeSeconds({ from, to }) {
2239
+ const ms = new Date(to).getTime() - new Date(from).getTime();
2240
+ return Number.isFinite(ms) ? Math.max(1, Math.round(ms / 1e3)) : 1;
2241
+ }
2242
+ /**
2243
+ * LogQL that counts log lines per value of one label across a time range:
2244
+ *
2245
+ * ```
2246
+ * sum by (response_code) (count_over_time({route_name="my-alb"}[1800s]))
2247
+ * ```
2248
+ *
2249
+ * Run it as an instant query (`/loki/api/v1/query`) with `time` set to the
2250
+ * range's `to`, then pass the response to `facetFromLokiCounts`. Returns
2251
+ * `null` for a label name LogQL can't express. Works for indexed stream labels
2252
+ * and for structured metadata, which Loki can also group by.
2253
+ */
2254
+ function buildLogFacetCountQuery({ label, matchers, timeRange }) {
2255
+ if (!LABEL_NAME.test(label)) return null;
2256
+ return `sum by (${label}) (count_over_time(${buildLogQL({ matchers })}[${rangeSeconds(timeRange)}s]))`;
2257
+ }
2258
+ /**
2259
+ * Turn the response to a `buildLogFacetCountQuery` query into a facet for
2260
+ * `Logs.Root`. Series without the label (lines that don't carry it) and zero
2261
+ * counts are skipped. Options are ordered the same way as `facetsFromEntries`.
2262
+ * Pass `label` to override the display name.
2263
+ */
2264
+ function facetFromLokiCounts(name, response, options = {}) {
2265
+ const counts = /* @__PURE__ */ new Map();
2266
+ if (response?.status === "success" && response.data?.resultType === "vector") for (const sample of response.data.result) {
2267
+ const value = sample.metric[name];
2268
+ const count = Number(sample.value?.[1]);
2269
+ if (!value || !Number.isFinite(count) || count <= 0) continue;
2270
+ counts.set(value, (counts.get(value) ?? 0) + Math.round(count));
2271
+ }
2272
+ return {
2273
+ name,
2274
+ label: options.label ?? facetLabel(name),
2275
+ options: [...counts.entries()].sort(([a], [b]) => compareFacetValues(name, a, b)).map(([value, count]) => ({
2276
+ value,
2277
+ count
2278
+ }))
2279
+ };
2280
+ }
1984
2281
  // src/components/features/logs/utils/flatten-loki.ts
1985
2282
  function nsToDate(timestampNs) {
1986
2283
  try {
@@ -2024,4 +2321,4 @@ function flattenLokiStreams(response) {
2024
2321
  });
2025
2322
  return entries;
2026
2323
  }
2027
- export { LOG_TIME_PRESETS, Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, logRequestHost, logTimeRangeLabel, logUserAgent, nsToDate, parseLogLine, parseUserAgent, resolveLogColumns, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, useLogs };
2324
+ export { LOGS_URL_KEYS, LOG_TIME_PRESETS, Logs, buildLogFacetCountQuery, buildLogQL, facetFromLokiCounts, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, logRequestHost, logTimeRangeLabel, logUserAgent, nsToDate, parseLogLine, parseLogsUrlState, parseUserAgent, resolveLogColumns, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, useLogs, useLogsUrlState, writeLogsUrlState };
@@ -1 +1 @@
1
- export { k as Map, b as MapCircle, x as MapCircleMarker, h as MapControlContainer, o as MapDrawCircle, s as MapDrawControl, c as MapDrawDelete, l as MapDrawEdit, u as MapDrawMarker, d as MapDrawPolygon, f as MapDrawPolyline, p as MapDrawRectangle, m as MapDrawUndo, t as MapFeatureGroup, g as MapFullscreenControl, n as MapLayerGroup, r as MapLayers, i as MapLayersControl, _ as MapLocateControl, S as MapMarker, C as MapMarkerClusterGroup, w as MapPolygon, T as MapPolyline, E as MapPopup, D as MapRectangle, v as MapSearchControl, a as MapTileLayer, O as MapTooltip, y as MapZoomControl, j as PlaceAutocomplete, A as useLeaflet } from "../layers-XEND2WKv.mjs";
1
+ export { k as Map, b as MapCircle, x as MapCircleMarker, h as MapControlContainer, o as MapDrawCircle, s as MapDrawControl, c as MapDrawDelete, l as MapDrawEdit, u as MapDrawMarker, d as MapDrawPolygon, f as MapDrawPolyline, p as MapDrawRectangle, m as MapDrawUndo, t as MapFeatureGroup, g as MapFullscreenControl, n as MapLayerGroup, r as MapLayers, i as MapLayersControl, _ as MapLocateControl, S as MapMarker, C as MapMarkerClusterGroup, w as MapPolygon, T as MapPolyline, E as MapPopup, D as MapRectangle, v as MapSearchControl, a as MapTileLayer, O as MapTooltip, y as MapZoomControl, j as PlaceAutocomplete, A as useLeaflet } from "../layers-BC1Ny49w.mjs";
@@ -1,3 +1,3 @@
1
1
  import { t as ActionRow } from "../action-row-BEzE9x37.mjs";
2
- import { t as MoreActions } from "../more-actions-D9k1ILzT.mjs";
2
+ import { t as MoreActions } from "../more-actions-DXQiV-bN.mjs";
3
3
  export { ActionRow, MoreActions };
@@ -1,7 +1,7 @@
1
- import { t as Button } from "./button-Dj6cKrWj.mjs";
1
+ import { t as Button } from "./button-Cn4KagBO.mjs";
2
2
  import { t as Tooltip } from "./tooltip-OIySjJWW.mjs";
3
3
  import { t as cn } from "./utils-CTIuhCOs.mjs";
4
- import { t as ResponsiveDropdown } from "./responsive-dropdown-DsWgpE_i.mjs";
4
+ import { t as ResponsiveDropdown } from "./responsive-dropdown-B9cDUeHt.mjs";
5
5
  import { t as ActionRow } from "./action-row-BEzE9x37.mjs";
6
6
  import { Ellipsis } from "lucide-react";
7
7
  import { useState } from "react";
@@ -0,0 +1,52 @@
1
+ import { n as EASE, t as DURATION } from "./motion-DOrRbLmy.mjs";
2
+ import { Badge } from "./badge/index.mjs";
3
+ import { motion, useReducedMotion } from "motion/react";
4
+ // src/components/features/_shared/motion-chip.tsx
5
+ /** `Badge` as a Motion component, so chips can enter, exit and reflow. */
6
+ const MotionBadge = motion.create(Badge);
7
+ const ENTER = {
8
+ duration: DURATION.fast,
9
+ ease: EASE.out
10
+ };
11
+ const EXIT = {
12
+ duration: .1,
13
+ ease: EASE.out
14
+ };
15
+ /** Neighbours slide into a removed chip's gap a touch slower than it fades. */
16
+ const REFLOW = {
17
+ duration: .2,
18
+ ease: EASE.out
19
+ };
20
+ /**
21
+ * Motion props for a chip in a wrapping list, for use with `MotionBadge`
22
+ * inside `<AnimatePresence initial={false} mode="popLayout">`.
23
+ *
24
+ * A chip scales up slightly as it fades in, and fades out a little faster
25
+ * when removed. `popLayout` takes the leaving chip out of flow straight away,
26
+ * and `layout="position"` slides the chips after it into the gap rather than
27
+ * snapping. The list needs `position: relative` for `popLayout` to place the
28
+ * leaving chip. Under reduced motion chips only fade and nothing slides.
29
+ */
30
+ function useChipMotion() {
31
+ const reduceMotion = Boolean(useReducedMotion());
32
+ const scale = reduceMotion ? 1 : .85;
33
+ return {
34
+ initial: {
35
+ opacity: 0,
36
+ scale
37
+ },
38
+ animate: {
39
+ opacity: 1,
40
+ scale: 1,
41
+ transition: ENTER
42
+ },
43
+ exit: {
44
+ opacity: 0,
45
+ scale,
46
+ transition: EXIT
47
+ },
48
+ transition: { layout: REFLOW },
49
+ layout: reduceMotion ? false : "position"
50
+ };
51
+ }
52
+ export { useChipMotion as n, MotionBadge as t };