@datum-cloud/datum-ui 2.6.0 → 2.7.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.
@@ -2,4 +2,4 @@ export { resolveLogColumns } from './components/columns';
2
2
  export { useLogs } from './hooks/use-logs';
3
3
  export { Logs } from './logs';
4
4
  export type { BuildLogQLOptions, LogColumn, LogColumnCellContext, LogColumnId, LogColumnSize, LogColumnSkeletonContext, LogColumnSpec, LogEntry, LogFacet, LogFacetOption, LogFilters, LogHistogramBucket, LogsContextValue, LogsRootProps, LogTimeRange, LokiQueryRangeResponse, LokiStream, ParsedHttpLogLine, ParsedLogLine, ParsedTextLogLine, } from './types';
5
- export { buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, LOG_TIME_PRESETS, logLineDisplay, logRequestHost, logTimeRangeLabel, nsToDate, parseLogLine, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, } from './utils';
5
+ export { buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, LOG_TIME_PRESETS, logLineDisplay, logRequestHost, logTimeRangeLabel, logUserAgent, nsToDate, parseLogLine, parseUserAgent, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, } from './utils';
@@ -8,3 +8,5 @@ export { logRequestHost } from './host';
8
8
  export { formatHttpLogLine, logLineDisplay, parseLogLine, splitPathQuery } from './parse-log-line';
9
9
  export { httpStatusBadgeType, httpStatusTextClass, severityBadgeType } from './severity';
10
10
  export { filtersAreActive, lastThirtyMinutes, LOG_TIME_PRESETS, logTimeRangeLabel, resolveLogTimeRange, } from './time-range';
11
+ export { logUserAgent, parseUserAgent } from './user-agent';
12
+ export type { ParsedUserAgent, UserAgentDevice } from './user-agent';
@@ -0,0 +1,25 @@
1
+ export type UserAgentDevice = 'desktop' | 'mobile' | 'tablet' | 'bot' | 'unknown';
2
+ export interface ParsedUserAgent {
3
+ /** Browser or client name, e.g. "Chrome", "Safari", "curl". */
4
+ browser?: string;
5
+ /** Major version of the browser or client, e.g. "128". */
6
+ browserVersion?: string;
7
+ /** Operating system, e.g. "macOS", "iOS", "Android", "Windows". */
8
+ os?: string;
9
+ /** OS version where it is meaningful (iOS, Android). Desktop OS versions are frozen or ambiguous in UA strings. */
10
+ osVersion?: string;
11
+ device: UserAgentDevice;
12
+ /** Human summary such as "Chrome 128 on macOS" or "curl 8.7". */
13
+ summary: string;
14
+ raw: string;
15
+ }
16
+ /** The User-Agent header from an entry's labels, if any. */
17
+ export declare function logUserAgent(labels: Record<string, string>): string | undefined;
18
+ /**
19
+ * Parse a User-Agent header into browser, OS and device type. Deliberately
20
+ * small: it recognises the mainstream browsers, mobile platforms, common
21
+ * command-line / SDK clients, and anything that calls itself a bot. Returns
22
+ * `null` for an empty string; unrecognised strings still return `summary`
23
+ * as the raw value with `device: 'unknown'`.
24
+ */
25
+ export declare function parseUserAgent(raw: string): ParsedUserAgent | null;
@@ -12,7 +12,7 @@ import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table
12
12
  import { t as DateTimeRangePicker } from "../date-time-range-picker-DlFTh3DF.mjs";
13
13
  import { c as getBrowserTimezone, t as DateTime, u as getTimezoneAbbreviation } from "../date-time-lZdefM4H.mjs";
14
14
  import { t as useCopyToClipboard } from "../use-copy-to-clipboard-DZDq_85T.mjs";
15
- import { ChevronDown, ChevronUp, Clock, Copy, Download, Inbox, RefreshCw, Search, SlidersHorizontal, TriangleAlert, X } from "lucide-react";
15
+ import { Bot, ChevronDown, ChevronUp, Clock, Copy, Download, HelpCircle, Inbox, Monitor, RefreshCw, Search, SlidersHorizontal, Smartphone, Tablet, TriangleAlert, X } from "lucide-react";
16
16
  import { createContext, memo, use, useCallback, useEffect, useMemo, useRef, useState } from "react";
17
17
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
18
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
@@ -438,7 +438,238 @@ function stripOnce(line, token) {
438
438
  return `${line.slice(0, index)}${line.slice(index + token.length)}`.replace(/\s+/g, " ").trim();
439
439
  }
440
440
  //#endregion
441
+ //#region src/components/features/logs/utils/user-agent.ts
442
+ /** Label keys, in order, that may carry the client's User-Agent header. */
443
+ const USER_AGENT_LABEL_KEYS = [
444
+ "user_agent",
445
+ "http_user_agent",
446
+ "useragent",
447
+ "ua",
448
+ "http.user_agent"
449
+ ];
450
+ /** The User-Agent header from an entry's labels, if any. */
451
+ function logUserAgent(labels) {
452
+ for (const key of USER_AGENT_LABEL_KEYS) {
453
+ const value = labels[key]?.trim();
454
+ if (value) return value;
455
+ }
456
+ }
457
+ const BOT_RE = /bot|crawl|spider|slurp|facebookexternalhit|preview|monitor|uptime|pingdom|kube-probe|prometheus|blackbox|healthcheck|health-check|datadog|newrelic|statuscake/i;
458
+ /** Order matters: forks embed the names of what they fork. */
459
+ const CLIENT_RULES = [
460
+ {
461
+ name: "curl",
462
+ re: /\bcurl\/(\d+(?:\.\d+)*)/i,
463
+ tool: true
464
+ },
465
+ {
466
+ name: "Wget",
467
+ re: /\bWget\/(\d+(?:\.\d+)*)/i,
468
+ tool: true
469
+ },
470
+ {
471
+ name: "Python Requests",
472
+ re: /\bpython-requests\/(\d+(?:\.\d+)*)/i,
473
+ tool: true
474
+ },
475
+ {
476
+ name: "Python urllib",
477
+ re: /\bPython-urllib\/(\d+(?:\.\d+)*)/i,
478
+ tool: true
479
+ },
480
+ {
481
+ name: "Go HTTP client",
482
+ re: /\bGo-http-client\/(\d+(?:\.\d+)*)/i,
483
+ tool: true
484
+ },
485
+ {
486
+ name: "okhttp",
487
+ re: /\bokhttp\/(\d+(?:\.\d+)*)/i,
488
+ tool: true
489
+ },
490
+ {
491
+ name: "axios",
492
+ re: /\baxios\/(\d+(?:\.\d+)*)/i,
493
+ tool: true
494
+ },
495
+ {
496
+ name: "node-fetch",
497
+ re: /\bnode-fetch\/(\d+(?:\.\d+)*)/i,
498
+ tool: true
499
+ },
500
+ {
501
+ name: "Node.js",
502
+ re: /\bnode\/v?(\d+(?:\.\d+)*)/i,
503
+ tool: true
504
+ },
505
+ {
506
+ name: "Postman",
507
+ re: /\bPostmanRuntime\/(\d+(?:\.\d+)*)/i,
508
+ tool: true
509
+ },
510
+ {
511
+ name: "Insomnia",
512
+ re: /\binsomnia\/(\d+(?:\.\d+)*)/i,
513
+ tool: true
514
+ },
515
+ {
516
+ name: "Java",
517
+ re: /\bJava\/(\d+(?:\.\d+)*)/i,
518
+ tool: true
519
+ },
520
+ {
521
+ name: "Edge",
522
+ re: /\bEdgA?\/(\d+(?:\.\d+)*)/
523
+ },
524
+ {
525
+ name: "Edge",
526
+ re: /\bEdgiOS\/(\d+(?:\.\d+)*)/
527
+ },
528
+ {
529
+ name: "Opera",
530
+ re: /\bOPR\/(\d+(?:\.\d+)*)/
531
+ },
532
+ {
533
+ name: "Opera",
534
+ re: /\bOpera\/(\d+(?:\.\d+)*)/
535
+ },
536
+ {
537
+ name: "Samsung Internet",
538
+ re: /\bSamsungBrowser\/(\d+(?:\.\d+)*)/
539
+ },
540
+ {
541
+ name: "Brave",
542
+ re: /\bBrave\/(\d+(?:\.\d+)*)/
543
+ },
544
+ {
545
+ name: "Vivaldi",
546
+ re: /\bVivaldi\/(\d+(?:\.\d+)*)/
547
+ },
548
+ {
549
+ name: "Firefox",
550
+ re: /\b(?:Firefox|FxiOS)\/(\d+(?:\.\d+)*)/
551
+ },
552
+ {
553
+ name: "Chrome",
554
+ re: /\b(?:Chrome|CriOS|HeadlessChrome)\/(\d+(?:\.\d+)*)/
555
+ }
556
+ ];
557
+ /** Safari carries its version under `Version/`; WebKit views on iOS have neither. */
558
+ function detectSafari(ua) {
559
+ if (!/\bSafari\//.test(ua) && !/\bAppleWebKit\//.test(ua)) return void 0;
560
+ const version = /\bVersion\/(\d+(?:\.\d+)*)/.exec(ua);
561
+ if (version && /\bSafari\//.test(ua)) return { version: majorVersion(version[1]) };
562
+ if (/\b(?:iPhone|iPad|iPod)\b/.test(ua)) return {};
563
+ }
564
+ function majorVersion(version) {
565
+ if (!version) return void 0;
566
+ return version.split(".")[0];
567
+ }
568
+ function detectOs(ua) {
569
+ let match;
570
+ if (/\b(?:iPhone|iPad|iPod)\b/i.test(ua)) {
571
+ match = /\bOS (\d+)[._](\d+)(?:[._](\d+))?/.exec(ua);
572
+ if (!match) return { os: "iOS" };
573
+ const patch = match[3] && match[3] !== "0" ? `.${match[3]}` : "";
574
+ return {
575
+ os: "iOS",
576
+ osVersion: `${match[1]}.${match[2]}${patch}`
577
+ };
578
+ }
579
+ match = /\bAndroid[ /](\d+(?:\.\d+)?)/i.exec(ua);
580
+ if (match) return {
581
+ os: "Android",
582
+ osVersion: match[1]
583
+ };
584
+ if (/\bAndroid\b/i.test(ua)) return { os: "Android" };
585
+ if (/\bWindows Phone\b/i.test(ua)) return { os: "Windows Phone" };
586
+ if (/\bWindows\b/i.test(ua)) return { os: "Windows" };
587
+ if (/\bCrOS\b/.test(ua)) return { os: "ChromeOS" };
588
+ if (/\bMac OS X\b|\bMacintosh\b/i.test(ua)) return { os: "macOS" };
589
+ if (/\bLinux\b|\bX11\b/i.test(ua)) return { os: "Linux" };
590
+ if (/\bFreeBSD\b/i.test(ua)) return { os: "FreeBSD" };
591
+ return {};
592
+ }
593
+ function detectDevice(ua, os, bot, tool) {
594
+ if (bot || tool) return "bot";
595
+ if (/\biPad\b|\bTablet\b|\bKindle\b|\bSilk\b/i.test(ua)) return "tablet";
596
+ if (os === "Android" && !/\bMobile\b/i.test(ua)) return "tablet";
597
+ if (/\bMobile\b|\biPhone\b|\biPod\b|\bWindows Phone\b/i.test(ua)) return "mobile";
598
+ if (os === "iOS") return "mobile";
599
+ if (os === "Windows" || os === "macOS" || os === "Linux" || os === "ChromeOS" || os === "FreeBSD") return "desktop";
600
+ return "unknown";
601
+ }
602
+ function botClient(ua) {
603
+ const match = /([\w.-]*(?:bot|crawl|spider|probe|monitor|uptime|check)[\w.-]*)(?:\/(\d+(?:\.\d+)*))?/i.exec(ua);
604
+ if (!match?.[1]) return void 0;
605
+ return {
606
+ name: match[1],
607
+ version: majorVersion(match[2])
608
+ };
609
+ }
610
+ /**
611
+ * Parse a User-Agent header into browser, OS and device type. Deliberately
612
+ * small: it recognises the mainstream browsers, mobile platforms, common
613
+ * command-line / SDK clients, and anything that calls itself a bot. Returns
614
+ * `null` for an empty string; unrecognised strings still return `summary`
615
+ * as the raw value with `device: 'unknown'`.
616
+ */
617
+ function parseUserAgent(raw) {
618
+ const ua = raw.trim();
619
+ if (!ua) return null;
620
+ let browser;
621
+ let browserVersion;
622
+ let tool = false;
623
+ for (const rule of CLIENT_RULES) {
624
+ const match = rule.re.exec(ua);
625
+ if (match) {
626
+ browser = rule.name;
627
+ browserVersion = majorVersion(match[1]);
628
+ tool = Boolean(rule.tool);
629
+ break;
630
+ }
631
+ }
632
+ if (!browser) {
633
+ const safari = detectSafari(ua);
634
+ if (safari) {
635
+ browser = "Safari";
636
+ browserVersion = safari.version;
637
+ }
638
+ }
639
+ const { os, osVersion } = tool ? {} : detectOs(ua);
640
+ const bot = !tool && BOT_RE.test(ua);
641
+ const device = detectDevice(ua, os, bot, tool);
642
+ if (bot && !browser) {
643
+ const named = botClient(ua);
644
+ browser = named?.name;
645
+ browserVersion = named?.version;
646
+ }
647
+ const client = browser ? browserVersion ? `${browser} ${browserVersion}` : browser : void 0;
648
+ const platform = os ? osVersion ? `${os} ${osVersion}` : os : void 0;
649
+ let summary;
650
+ if (client && platform) summary = `${client} on ${platform}`;
651
+ else if (client) summary = client;
652
+ else if (platform) summary = platform;
653
+ else summary = ua;
654
+ return {
655
+ browser,
656
+ browserVersion,
657
+ os,
658
+ osVersion,
659
+ device,
660
+ summary,
661
+ raw: ua
662
+ };
663
+ }
664
+ //#endregion
441
665
  //#region src/components/features/logs/components/detail.tsx
666
+ const DEVICE_ICONS = {
667
+ desktop: Monitor,
668
+ mobile: Smartphone,
669
+ tablet: Tablet,
670
+ bot: Bot,
671
+ unknown: HelpCircle
672
+ };
442
673
  function MetaCell({ label, children, className }) {
443
674
  return /* @__PURE__ */ jsxs("div", {
444
675
  className: cn("bg-background min-w-0 px-3 py-2.5", className),
@@ -474,6 +705,8 @@ const DetailBody = memo(({ showClose = true }) => {
474
705
  const request = splitPathQuery(parsed.kind === "http" ? parsed.path : "");
475
706
  const title = parsed.kind === "http" ? `${parsed.method} ${request.pathname}` : selectedEntry.labels.service_name ?? "Log";
476
707
  const service = selectedEntry.labels.service_name ?? logRequestHost(selectedEntry.labels);
708
+ const userAgent = logUserAgent(selectedEntry.labels);
709
+ const client = userAgent ? parseUserAgent(userAgent) : null;
477
710
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("header", {
478
711
  className: "border-b px-4 py-3",
479
712
  children: [/* @__PURE__ */ jsxs("div", {
@@ -593,6 +826,53 @@ const DetailBody = memo(({ showClose = true }) => {
593
826
  className: "col-span-3",
594
827
  children: request.pathname
595
828
  }),
829
+ client && /* @__PURE__ */ jsxs("div", {
830
+ className: "bg-background col-span-3 min-w-0 px-3 py-2.5",
831
+ "data-slot": "logs-client",
832
+ children: [
833
+ /* @__PURE__ */ jsxs("div", {
834
+ className: "mb-1 flex items-center justify-between",
835
+ children: [/* @__PURE__ */ jsx("span", {
836
+ className: "text-muted-foreground text-[11px] font-medium",
837
+ children: "Client"
838
+ }), /* @__PURE__ */ jsx(Button, {
839
+ type: "secondary",
840
+ theme: "borderless",
841
+ size: "icon",
842
+ className: "-my-1 size-6",
843
+ "aria-label": "Copy user agent",
844
+ onClick: () => copy(client.raw, { withToast: true }),
845
+ children: /* @__PURE__ */ jsx(Icon, {
846
+ icon: Copy,
847
+ size: 14
848
+ })
849
+ })]
850
+ }),
851
+ /* @__PURE__ */ jsxs("div", {
852
+ className: "flex items-center gap-2 text-xs",
853
+ children: [
854
+ /* @__PURE__ */ jsx(Icon, {
855
+ icon: DEVICE_ICONS[client.device],
856
+ className: "text-muted-foreground shrink-0"
857
+ }),
858
+ /* @__PURE__ */ jsx("span", {
859
+ className: "min-w-0 truncate font-medium",
860
+ "data-slot": "logs-client-summary",
861
+ children: client.summary
862
+ }),
863
+ /* @__PURE__ */ jsx("span", {
864
+ className: "text-muted-foreground shrink-0 text-[11px] capitalize",
865
+ children: client.device === "unknown" ? "" : client.device
866
+ })
867
+ ]
868
+ }),
869
+ client.summary !== client.raw && /* @__PURE__ */ jsx("p", {
870
+ className: "text-muted-foreground mt-1 font-mono text-[11px] leading-4 break-all",
871
+ title: client.raw,
872
+ children: client.raw
873
+ })
874
+ ]
875
+ }),
596
876
  request.params.length > 0 && /* @__PURE__ */ jsxs("div", {
597
877
  className: "bg-background col-span-3 min-w-0 px-3 py-2.5",
598
878
  "data-slot": "logs-search-params",
@@ -984,36 +1264,32 @@ function LogsFilters({ className }) {
984
1264
  //#endregion
985
1265
  //#region src/components/features/logs/components/table.tsx
986
1266
  const SKELETON_ROWS = 12;
987
- /** Centred message row shared by the empty and error states. */
988
- function LogsTableState({ icon, title, description, action, colSpan, slot, role, tone = "muted" }) {
989
- return /* @__PURE__ */ jsx(TableRow, {
990
- className: "hover:bg-transparent",
991
- children: /* @__PURE__ */ jsx(TableCell, {
992
- colSpan,
993
- className: "pt-20 pb-12 text-center",
994
- children: /* @__PURE__ */ jsxs("div", {
995
- role,
996
- "data-slot": slot,
997
- className: "mx-auto flex max-w-md flex-col items-center gap-2 px-4",
998
- children: [
999
- /* @__PURE__ */ jsx("span", {
1000
- className: cn("flex size-9 items-center justify-center rounded-full border", tone === "destructive" ? "border-destructive/20 bg-destructive/10 text-destructive" : "border-border bg-muted text-muted-foreground"),
1001
- children: icon
1002
- }),
1003
- /* @__PURE__ */ jsx("p", {
1004
- className: "text-foreground text-sm font-medium",
1005
- children: title
1006
- }),
1007
- description && /* @__PURE__ */ jsx("p", {
1008
- className: "text-muted-foreground text-xs",
1009
- children: description
1010
- }),
1011
- action && /* @__PURE__ */ jsx("div", {
1012
- className: "mt-1.5",
1013
- children: action
1014
- })
1015
- ]
1016
- })
1267
+ /** Centred message shared by the empty and error states. */
1268
+ function LogsTableState({ icon, title, description, action, slot, role, tone = "muted" }) {
1269
+ return /* @__PURE__ */ jsx("div", {
1270
+ className: "flex min-h-48 flex-1 items-center justify-center px-4 py-8 text-center",
1271
+ children: /* @__PURE__ */ jsxs("div", {
1272
+ role,
1273
+ "data-slot": slot,
1274
+ className: "flex max-w-md flex-col items-center gap-2",
1275
+ children: [
1276
+ /* @__PURE__ */ jsx("span", {
1277
+ className: cn("flex size-9 items-center justify-center rounded-full border", tone === "destructive" ? "border-destructive/20 bg-destructive/10 text-destructive" : "border-border bg-muted text-muted-foreground"),
1278
+ children: icon
1279
+ }),
1280
+ /* @__PURE__ */ jsx("p", {
1281
+ className: "text-foreground text-sm font-medium",
1282
+ children: title
1283
+ }),
1284
+ description && /* @__PURE__ */ jsx("p", {
1285
+ className: "text-muted-foreground text-xs",
1286
+ children: description
1287
+ }),
1288
+ action && /* @__PURE__ */ jsx("div", {
1289
+ className: "mt-1.5",
1290
+ children: action
1291
+ })
1292
+ ]
1017
1293
  })
1018
1294
  });
1019
1295
  }
@@ -1068,92 +1344,62 @@ function LogsTable({ className }) {
1068
1344
  return /* @__PURE__ */ jsxs("div", {
1069
1345
  ref: scrollerRef,
1070
1346
  "data-slot": "logs-table",
1071
- className: cn("min-h-0 flex-1 overflow-auto outline-none", className),
1347
+ className: cn("flex min-h-0 flex-1 flex-col overflow-auto outline-none", className),
1072
1348
  tabIndex: 0,
1073
1349
  "aria-busy": isLoading || void 0,
1074
1350
  onKeyDown,
1075
- children: [showErrorBanner && /* @__PURE__ */ jsxs("div", {
1076
- role: "alert",
1077
- "data-slot": "logs-error-banner",
1078
- className: "border-destructive/30 bg-destructive/5 text-foreground sticky top-0 z-20 flex items-center gap-2 border-b px-3 py-1.5 text-xs",
1079
- children: [
1080
- /* @__PURE__ */ jsx(Icon, {
1081
- icon: TriangleAlert,
1082
- size: 14,
1083
- className: "text-destructive shrink-0"
1084
- }),
1085
- /* @__PURE__ */ jsxs("span", {
1086
- className: "min-w-0 truncate",
1087
- children: [
1088
- /* @__PURE__ */ jsx("span", {
1089
- className: "font-medium",
1090
- children: "Refresh failed."
1091
- }),
1092
- " ",
1093
- /* @__PURE__ */ jsx("span", {
1094
- className: "text-muted-foreground",
1095
- children: "Showing the last successful result."
1096
- }),
1097
- " ",
1098
- /* @__PURE__ */ jsx("code", {
1099
- className: "font-mono",
1100
- children: error
1101
- })
1102
- ]
1103
- }),
1104
- /* @__PURE__ */ jsx("span", {
1105
- className: "ml-auto shrink-0",
1106
- children: retry
1107
- })
1108
- ]
1109
- }), /* @__PURE__ */ jsxs("table", {
1110
- className: "w-full caption-bottom table-auto text-sm [&_th]:px-3 [&_th]:py-2 [&_td]:px-3 [&_td]:py-1.5",
1111
- children: [/* @__PURE__ */ jsx(TableHeader, {
1112
- className: "bg-background sticky top-0 z-10",
1113
- children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
1114
- className: cn(columnHeadClass(column), column.headerClassName),
1115
- style: columnWidthStyle(column),
1116
- children: columnHeader(column)
1117
- }, column.id)) })
1118
- }), /* @__PURE__ */ jsxs(TableBody, { children: [
1119
- showSkeleton && Array.from({ length: SKELETON_ROWS }, (_, index) => /* @__PURE__ */ jsx(TableRow, {
1351
+ children: [
1352
+ showErrorBanner && /* @__PURE__ */ jsxs("div", {
1353
+ role: "alert",
1354
+ "data-slot": "logs-error-banner",
1355
+ className: "border-destructive/30 bg-destructive/5 text-foreground sticky top-0 z-20 flex items-center gap-2 border-b px-3 py-1.5 text-xs",
1356
+ children: [
1357
+ /* @__PURE__ */ jsx(Icon, {
1358
+ icon: TriangleAlert,
1359
+ size: 14,
1360
+ className: "text-destructive shrink-0"
1361
+ }),
1362
+ /* @__PURE__ */ jsxs("span", {
1363
+ className: "min-w-0 truncate",
1364
+ children: [
1365
+ /* @__PURE__ */ jsx("span", {
1366
+ className: "font-medium",
1367
+ children: "Refresh failed."
1368
+ }),
1369
+ " ",
1370
+ /* @__PURE__ */ jsx("span", {
1371
+ className: "text-muted-foreground",
1372
+ children: "Showing the last successful result."
1373
+ }),
1374
+ " ",
1375
+ /* @__PURE__ */ jsx("code", {
1376
+ className: "font-mono",
1377
+ children: error
1378
+ })
1379
+ ]
1380
+ }),
1381
+ /* @__PURE__ */ jsx("span", {
1382
+ className: "ml-auto shrink-0",
1383
+ children: retry
1384
+ })
1385
+ ]
1386
+ }),
1387
+ /* @__PURE__ */ jsxs("table", {
1388
+ className: "w-full shrink-0 caption-bottom table-auto text-sm [&_th]:px-3 [&_th]:py-2 [&_td]:px-3 [&_td]:py-1.5",
1389
+ children: [/* @__PURE__ */ jsx(TableHeader, {
1390
+ className: "bg-background sticky top-0 z-10",
1391
+ children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
1392
+ className: cn(columnHeadClass(column), column.headerClassName),
1393
+ style: columnWidthStyle(column),
1394
+ children: columnHeader(column)
1395
+ }, column.id)) })
1396
+ }), /* @__PURE__ */ jsxs(TableBody, { children: [showSkeleton && Array.from({ length: SKELETON_ROWS }, (_, index) => /* @__PURE__ */ jsx(TableRow, {
1120
1397
  "data-slot": "logs-skeleton-row",
1121
1398
  children: columns.map((column) => /* @__PURE__ */ jsx(LogCell, {
1122
1399
  column,
1123
1400
  children: columnSkeleton(column, index)
1124
1401
  }, column.id))
1125
- }, `skeleton-${index}`)),
1126
- showError && /* @__PURE__ */ jsx(LogsTableState, {
1127
- role: "alert",
1128
- slot: "logs-error",
1129
- tone: "destructive",
1130
- colSpan: columns.length,
1131
- icon: /* @__PURE__ */ jsx(Icon, { icon: TriangleAlert }),
1132
- title: "Couldn't load logs",
1133
- description: /* @__PURE__ */ jsx("code", {
1134
- className: "bg-muted text-foreground inline-block max-w-full rounded-md border px-2 py-1 text-left font-mono text-[11px] leading-4 break-all",
1135
- children: error
1136
- }),
1137
- action: retry
1138
- }),
1139
- showEmpty && /* @__PURE__ */ jsx(LogsTableState, {
1140
- slot: "logs-empty",
1141
- colSpan: columns.length,
1142
- icon: /* @__PURE__ */ jsx(Icon, { icon: Inbox }),
1143
- title: "No logs in this time range",
1144
- description: hasActiveFilters || search ? "Try widening the range or clearing the filters." : "Try widening the range or check back in a moment.",
1145
- action: (hasActiveFilters || search) && /* @__PURE__ */ jsx(Button, {
1146
- type: "secondary",
1147
- theme: "outline",
1148
- size: "small",
1149
- onClick: () => {
1150
- resetFilters();
1151
- setSearch("");
1152
- },
1153
- children: "Clear filters"
1154
- })
1155
- }),
1156
- rows.map(({ entry, ctx }) => {
1402
+ }, `skeleton-${index}`)), rows.map(({ entry, ctx }) => {
1157
1403
  const selected = entry.id === selectedId;
1158
1404
  return /* @__PURE__ */ jsx(TableRow, {
1159
1405
  "data-state": selected ? "selected" : void 0,
@@ -1168,9 +1414,37 @@ function LogsTable({ className }) {
1168
1414
  children: column.cell(ctx)
1169
1415
  }, column.id))
1170
1416
  }, entry.id);
1417
+ })] })]
1418
+ }),
1419
+ showError && /* @__PURE__ */ jsx(LogsTableState, {
1420
+ role: "alert",
1421
+ slot: "logs-error",
1422
+ tone: "destructive",
1423
+ icon: /* @__PURE__ */ jsx(Icon, { icon: TriangleAlert }),
1424
+ title: "Couldn't load logs",
1425
+ description: /* @__PURE__ */ jsx("code", {
1426
+ className: "bg-muted text-foreground inline-block max-w-full rounded-md border px-2 py-1 text-left font-mono text-[11px] leading-4 break-all",
1427
+ children: error
1428
+ }),
1429
+ action: retry
1430
+ }),
1431
+ showEmpty && /* @__PURE__ */ jsx(LogsTableState, {
1432
+ slot: "logs-empty",
1433
+ icon: /* @__PURE__ */ jsx(Icon, { icon: Inbox }),
1434
+ title: "No logs in this time range",
1435
+ description: hasActiveFilters || search ? "Try widening the range or clearing the filters." : "Try widening the range or check back in a moment.",
1436
+ action: (hasActiveFilters || search) && /* @__PURE__ */ jsx(Button, {
1437
+ type: "secondary",
1438
+ theme: "outline",
1439
+ size: "small",
1440
+ onClick: () => {
1441
+ resetFilters();
1442
+ setSearch("");
1443
+ },
1444
+ children: "Clear filters"
1171
1445
  })
1172
- ] })]
1173
- })]
1446
+ })
1447
+ ]
1174
1448
  });
1175
1449
  }
1176
1450
  const MINUTE_MS = 6e4;
@@ -1772,4 +2046,4 @@ function flattenLokiStreams(response) {
1772
2046
  return entries;
1773
2047
  }
1774
2048
  //#endregion
1775
- export { LOG_TIME_PRESETS, Logs, buildLogQL, facetsFromEntries, filterEntries, flattenLokiStreams, formatHttpLogLine, histogramFromEntries, httpStatusBadgeType, httpStatusTextClass, lastThirtyMinutes, logLineDisplay, logRequestHost, logTimeRangeLabel, nsToDate, parseLogLine, resolveLogColumns, resolveLogTimeRange, searchableLogText, severityBadgeType, splitPathQuery, useLogs };
2049
+ 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datum-cloud/datum-ui",
3
3
  "type": "module",
4
- "version": "2.6.0",
4
+ "version": "2.7.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "url": "https://github.com/datum-cloud/datum-ui"