@sentry/junior-dashboard 0.265.0 → 0.267.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 (104) hide show
  1. package/dist/app.js +49 -12
  2. package/dist/client.js +94 -63
  3. package/dist/index.js +49 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +35 -53
  9. package/src/client/components/ActorAvatar.tsx +10 -22
  10. package/src/client/components/ConnectionBanner.tsx +4 -3
  11. package/src/client/components/CopyButton.tsx +3 -4
  12. package/src/client/components/DetailsPanel.tsx +12 -13
  13. package/src/client/components/Drawer.tsx +46 -23
  14. package/src/client/components/EmptyTelemetry.tsx +5 -3
  15. package/src/client/components/ImageAttachment.tsx +49 -63
  16. package/src/client/components/InlineError.tsx +8 -4
  17. package/src/client/components/Notice.tsx +19 -26
  18. package/src/client/components/Pagination.tsx +26 -26
  19. package/src/client/components/ParticipantAvatarStack.tsx +13 -10
  20. package/src/client/components/README.md +28 -3
  21. package/src/client/components/SearchInput.tsx +51 -41
  22. package/src/client/components/VersionDriftBanner.tsx +9 -7
  23. package/src/client/components/charts/CacheWrites.tsx +52 -43
  24. package/src/client/components/ui/alert.tsx +64 -0
  25. package/src/client/components/ui/avatar.tsx +53 -0
  26. package/src/client/components/ui/button-group.tsx +59 -0
  27. package/src/client/components/ui/button.tsx +2 -1
  28. package/src/client/components/ui/dialog.tsx +60 -0
  29. package/src/client/components/ui/drawer.tsx +140 -0
  30. package/src/client/components/ui/empty.tsx +61 -0
  31. package/src/client/components/ui/input-group.tsx +96 -0
  32. package/src/client/components/ui/pagination.tsx +60 -0
  33. package/src/client/components/ui/separator.tsx +22 -0
  34. package/src/client/components/ui/spinner.tsx +21 -0
  35. package/src/client/components/ui/table.tsx +22 -1
  36. package/src/client/components/ui/tooltip.tsx +1 -1
  37. package/src/client/components/ui/useSheet.ts +5 -1
  38. package/src/client/conversations/ComposerImages.tsx +66 -14
  39. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  40. package/src/client/conversations/ConversationComposer.tsx +84 -12
  41. package/src/client/conversations/ConversationDetails.tsx +3 -27
  42. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  43. package/src/client/conversations/ConversationHeader.tsx +5 -4
  44. package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
  45. package/src/client/conversations/ConversationPage.tsx +156 -2
  46. package/src/client/conversations/ConversationSidebar.tsx +5 -4
  47. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  48. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  49. package/src/client/conversations/ConversationWatches.tsx +67 -59
  50. package/src/client/conversations/DetailsList.tsx +77 -29
  51. package/src/client/conversations/EventDetails.tsx +46 -19
  52. package/src/client/conversations/EventUsage.tsx +99 -74
  53. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  54. package/src/client/conversations/PendingMailboxStack.tsx +312 -119
  55. package/src/client/conversations/README.md +30 -10
  56. package/src/client/conversations/ShareConversationButton.tsx +3 -4
  57. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  58. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  59. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  60. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  61. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  62. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  63. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  64. package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
  65. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  66. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  67. package/src/client/conversations/TranscriptSummary.tsx +13 -7
  68. package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
  69. package/src/client/conversations/TranscriptView.tsx +7 -3
  70. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  71. package/src/client/conversations/conversationOutbox.ts +46 -0
  72. package/src/client/conversations/eventLog.ts +1 -0
  73. package/src/client/conversations/eventTime.ts +13 -1
  74. package/src/client/conversations/eventTranscript.ts +12 -5
  75. package/src/client/conversations/queries.ts +127 -26
  76. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  77. package/src/client/conversations/transcriptMentions.tsx +18 -5
  78. package/src/client/conversations/userInputRequest.ts +56 -0
  79. package/src/client/format.ts +10 -3
  80. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  81. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  82. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  83. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  84. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  85. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  86. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  87. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  88. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  89. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  90. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  91. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  92. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  93. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  94. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  95. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  96. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  97. package/src/client/pages/automations/README.md +56 -46
  98. package/src/client/types.ts +3 -0
  99. package/src/mock-reporting/fixtures.ts +44 -2
  100. package/src/mock-reporting/watches.ts +1 -1
  101. package/src/client/conversations/transcriptStyles.ts +0 -10
  102. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  103. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  104. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
3
  import { cn } from "../styles";
4
+ import { Alert } from "./ui/alert";
4
5
 
5
6
  /** Render a compact error message with consistent alert semantics. */
6
7
  export function InlineError(props: {
@@ -8,11 +9,14 @@ export function InlineError(props: {
8
9
  className?: string;
9
10
  }) {
10
11
  return (
11
- <p
12
- className={cn("m-0 text-sm text-rose-300", props.className)}
13
- role="alert"
12
+ <Alert
13
+ className={cn(
14
+ "block rounded-none border-0 bg-transparent p-0 text-rose-300",
15
+ props.className,
16
+ )}
17
+ variant="destructive"
14
18
  >
15
19
  {props.children}
16
- </p>
20
+ </Alert>
17
21
  );
18
22
  }
@@ -1,7 +1,9 @@
1
- import type { ButtonHTMLAttributes, ReactNode } from "react";
1
+ import type { ComponentProps, ReactNode } from "react";
2
2
  import type { LucideIcon } from "lucide-react";
3
3
 
4
4
  import { cn } from "../styles";
5
+ import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
6
+ import { Button } from "./ui/button";
5
7
 
6
8
  type NoticeTone = "default" | "error";
7
9
 
@@ -14,7 +16,7 @@ type NoticeProps = {
14
16
  tone?: NoticeTone;
15
17
  };
16
18
 
17
- type NoticeActionProps = ButtonHTMLAttributes<HTMLButtonElement> & {
19
+ type NoticeActionProps = ComponentProps<typeof Button> & {
18
20
  tone?: NoticeTone;
19
21
  };
20
22
 
@@ -29,11 +31,10 @@ export function Notice({
29
31
  }: NoticeProps) {
30
32
  const isError = tone === "error";
31
33
  return (
32
- <div
33
- className={cn(
34
- "overflow-hidden rounded-xl border bg-dashboard-surface-raised shadow-[0_16px_40px_rgba(0,0,0,0.5)]",
35
- isError ? "border-rose-300/25" : "border-dashboard-border-strong",
36
- )}
34
+ <Alert
35
+ className="block gap-0 overflow-hidden rounded-xl p-0 shadow-[0_16px_40px_rgba(0,0,0,0.5)]"
36
+ role={isError ? "alert" : "status"}
37
+ variant={isError ? "destructive" : "default"}
37
38
  >
38
39
  <div className="flex min-w-0 items-center gap-3 px-3 py-3">
39
40
  {Icon ? (
@@ -48,31 +49,26 @@ export function Notice({
48
49
  <Icon aria-hidden="true" size={16} />
49
50
  </div>
50
51
  ) : null}
51
- <div className="min-w-0 flex-1" role={isError ? "alert" : "status"}>
52
- <div
53
- className={cn(
54
- "font-display text-sm font-medium leading-snug",
55
- isError ? "text-rose-50" : "text-dashboard-text",
56
- )}
57
- >
52
+ <div className="min-w-0 flex-1">
53
+ <AlertTitle className="font-display text-sm leading-snug">
58
54
  {title}
59
55
  {detail ? " " : null}
60
- </div>
56
+ </AlertTitle>
61
57
  {detail ? (
62
- <div
58
+ <AlertDescription
63
59
  className={cn(
64
60
  "mt-0.5 truncate font-sans text-xs",
65
- isError ? "text-rose-100/70" : "text-dashboard-text-muted",
61
+ isError && "text-rose-100/70",
66
62
  )}
67
63
  >
68
64
  {detail}
69
- </div>
65
+ </AlertDescription>
70
66
  ) : null}
71
67
  </div>
72
68
  {action}
73
69
  </div>
74
70
  {children}
75
- </div>
71
+ </Alert>
76
72
  );
77
73
  }
78
74
 
@@ -80,22 +76,19 @@ export function Notice({
80
76
  export function NoticeAction({
81
77
  className,
82
78
  tone = "default",
83
- type = "button",
84
79
  ...props
85
80
  }: NoticeActionProps) {
86
81
  const isError = tone === "error";
87
82
  return (
88
- <button
83
+ <Button
89
84
  {...props}
90
85
  className={cn(
91
- "shrink-0 cursor-pointer px-3 py-1.5 text-xs transition focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-40",
92
- isError ? "focus:ring-rose-300/40" : "focus:ring-dashboard-focus/35",
86
+ "h-auto shrink-0 rounded-lg border-0 px-3 py-1.5 text-xs leading-normal",
93
87
  isError
94
- ? "rounded-lg bg-rose-200/15 font-sans font-medium text-rose-50 hover:bg-rose-200/25"
95
- : "rounded-lg bg-dashboard-accent/10 font-sans font-medium text-dashboard-accent-text hover:bg-dashboard-accent/20",
88
+ ? "bg-rose-200/15 text-rose-50 hover:bg-rose-200/25 hover:text-rose-50 focus-visible:outline-rose-300/55 data-disabled:hover:bg-rose-200/15 data-disabled:hover:text-rose-50"
89
+ : "bg-dashboard-accent/10 text-dashboard-accent-text hover:bg-dashboard-accent/20 hover:text-dashboard-accent-text data-disabled:hover:bg-dashboard-accent/10 data-disabled:hover:text-dashboard-accent-text",
96
90
  className,
97
91
  )}
98
- type={type}
99
92
  />
100
93
  );
101
94
  }
@@ -1,7 +1,12 @@
1
- import { ChevronLeft, ChevronRight } from "lucide-react";
2
-
3
1
  import { cn } from "../styles";
4
2
  import { Button } from "./ui/button";
3
+ import {
4
+ Pagination,
5
+ PaginationContent,
6
+ PaginationItem,
7
+ PaginationNext,
8
+ PaginationPrevious,
9
+ } from "./ui/pagination";
5
10
 
6
11
  /** Render a centered load-more control for cursor or progressive lists. */
7
12
  export function LoadMorePagination(props: {
@@ -54,10 +59,9 @@ export function PagePagination(props: {
54
59
  const end = Math.min(props.total, current * props.pageSize);
55
60
 
56
61
  return (
57
- <div
58
- aria-label="Pagination"
62
+ <Pagination
59
63
  className={cn(
60
- "flex flex-wrap items-center justify-between gap-3",
64
+ "flex-wrap items-center justify-between gap-3",
61
65
  props.className,
62
66
  )}
63
67
  >
@@ -65,27 +69,23 @@ export function PagePagination(props: {
65
69
  Showing {start.toLocaleString("en-US")}-{end.toLocaleString("en-US")} of{" "}
66
70
  {props.total.toLocaleString("en-US")}
67
71
  </p>
68
- <div className="flex items-center gap-2">
69
- <Button
70
- aria-label="Previous page"
71
- disabled={current <= 1}
72
- onClick={() => props.onPageChange(current - 1)}
73
- size="icon"
74
- >
75
- <ChevronLeft aria-hidden="true" size={16} />
76
- </Button>
77
- <span className="min-w-16 text-center font-sans tabular-nums text-xs text-dashboard-text-muted">
72
+ <PaginationContent>
73
+ <PaginationItem>
74
+ <PaginationPrevious
75
+ disabled={current <= 1}
76
+ onClick={() => props.onPageChange(current - 1)}
77
+ />
78
+ </PaginationItem>
79
+ <PaginationItem className="min-w-16 text-center font-sans tabular-nums text-xs text-dashboard-text-muted">
78
80
  {current} / {props.pageCount}
79
- </span>
80
- <Button
81
- aria-label="Next page"
82
- disabled={current >= props.pageCount}
83
- onClick={() => props.onPageChange(current + 1)}
84
- size="icon"
85
- >
86
- <ChevronRight aria-hidden="true" size={16} />
87
- </Button>
88
- </div>
89
- </div>
81
+ </PaginationItem>
82
+ <PaginationItem>
83
+ <PaginationNext
84
+ disabled={current >= props.pageCount}
85
+ onClick={() => props.onPageChange(current + 1)}
86
+ />
87
+ </PaginationItem>
88
+ </PaginationContent>
89
+ </Pagination>
90
90
  );
91
91
  }
@@ -1,9 +1,9 @@
1
1
  import type { ActorIdentity } from "@sentry/junior/api/schema";
2
2
 
3
3
  import { actorLabel } from "../format";
4
- import { cn } from "../styles";
5
4
  import type { Conversation } from "../types";
6
5
  import { ActorAvatar } from "./ActorAvatar";
6
+ import { Avatar, AvatarFallback } from "./ui/avatar";
7
7
  import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
8
8
 
9
9
  const MAX_VISIBLE_PARTICIPANTS = 3;
@@ -51,7 +51,8 @@ function ParticipantTooltipContent(props: { participant: ActorIdentity }) {
51
51
  );
52
52
  }
53
53
 
54
- function Avatar(props: {
54
+ /** Show one actor avatar with their name and email on hover or focus. */
55
+ export function ParticipantAvatar(props: {
55
56
  participant: ActorIdentity;
56
57
  size: ParticipantAvatarStackProps["size"];
57
58
  }) {
@@ -62,6 +63,8 @@ function Avatar(props: {
62
63
  render={
63
64
  <span
64
65
  aria-label={label}
66
+ // A name on a generic span is often skipped by screen readers.
67
+ role="img"
65
68
  className="relative -ml-1 inline-flex rounded-full first:ml-0 pointer-events-none focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
66
69
  onKeyDown={(event) => event.stopPropagation()}
67
70
  tabIndex={0}
@@ -101,7 +104,7 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
101
104
  role="group"
102
105
  >
103
106
  {visible.map((participant, index) => (
104
- <Avatar
107
+ <ParticipantAvatar
105
108
  key={participantKey(participant, index)}
106
109
  participant={participant}
107
110
  size={props.size}
@@ -111,19 +114,19 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
111
114
  <Tooltip>
112
115
  <TooltipTrigger
113
116
  render={
114
- <span
117
+ <Avatar
115
118
  aria-label={`${hidden.length} more participants: ${hidden
116
119
  .map(participantDescription)
117
120
  .join("; ")}`}
118
- className={cn(
119
- "relative -ml-1 inline-grid shrink-0 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto place-items-center rounded-full border-2 border-dashboard-bg bg-dashboard-surface-active font-sans font-semibold tabular-nums leading-none text-dashboard-text-muted focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
120
- props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
121
- )}
121
+ className="-ml-1 pointer-events-none bg-dashboard-surface-active shadow-none focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
122
122
  onKeyDown={(event) => event.stopPropagation()}
123
+ size={props.size === "list" ? "sm" : "default"}
123
124
  tabIndex={0}
124
125
  >
125
- +{hidden.length}
126
- </span>
126
+ <AvatarFallback className="tabular-nums tracking-normal text-dashboard-text-muted">
127
+ +{hidden.length}
128
+ </AvatarFallback>
129
+ </Avatar>
127
130
  }
128
131
  />
129
132
  <TooltipContent>
@@ -32,9 +32,15 @@ file when every caller needs the same style or variant. App components compose
32
32
 
33
33
  | App component | shadcn source |
34
34
  | ------------------------------ | ----------------- |
35
- | `Drawer`, `BottomSheet` | `ui/sheet` |
35
+ | `Drawer` | `ui/drawer` |
36
+ | `BottomSheet` | `ui/sheet` |
36
37
  | `layout/DashboardHeader` (nav) | `ui/sheet` |
37
38
  | `ProfileMenu` (desktop) | `ui/popover` |
39
+ | `ActorAvatar` | `ui/avatar` |
40
+ | `ParticipantAvatarStack` | `ui/avatar` |
41
+ | `SearchInput` | `ui/input-group` |
42
+ | `PagePagination` | `ui/pagination` |
43
+ | `EmptyTelemetry` | `ui/empty` |
38
44
  | `TimeRangeSelector` | `ui/toggle-group` |
39
45
  | `ThemeSwitcher` | `ui/toggle-group` |
40
46
  | `TranscriptViewToggle` | `ui/toggle-group` |
@@ -42,6 +48,9 @@ file when every caller needs the same style or variant. App components compose
42
48
  | `charts/ActivityChartTooltip` | `ui/tooltip` |
43
49
  | `conversations/ToolFrame` | `ui/trace` |
44
50
  | `TranscriptReasoningView` | `ui/trace` |
51
+ | `ImageAttachment` (preview) | `ui/dialog` |
52
+ | `Notice`, `InlineError` | `ui/alert` |
53
+ | App banners | `ui/alert` |
45
54
  | Recharts activity charts | `ui/chart` |
46
55
 
47
56
  Rules for adopted components:
@@ -54,6 +63,14 @@ Rules for adopted components:
54
63
  - Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
55
64
  Keep the confirmed item in state after close, so the closing dialog keeps its
56
65
  text.
66
+ - Use `ui/dialog` for a short task form, such as the fork message. Do not put
67
+ a form in a `Drawer` or a `DetailsPanel`. On small screens the dialog sits
68
+ near the top, so the on-screen keyboard does not cover its fields.
69
+ - Use `useSheet` for each app `ui/dialog`, as for sheets. It shares the
70
+ document scroll lock. Only the front modal closes on Escape or a backdrop
71
+ tap. Mount the dialog only while it is open.
72
+ - `ui/alert` sets `role="alert"`. Pass `role="status"` for a message that is
73
+ not an error.
57
74
  - `ui/toast` has no global notification manager. Features call
58
75
  `useToastManager` inside the mounted `Toaster` provider.
59
76
  - Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
@@ -68,6 +85,8 @@ Rules for adopted components:
68
85
  - Build a form field from `ui/field`. Give `FieldError` the id
69
86
  `<control id>-error` and `FieldDescription` the id `<control id>-help`. The
70
87
  control sets `aria-invalid` and `aria-describedby`.
88
+ - Use `Button size="menu"` for a full-width icon and label row in a mobile
89
+ overflow menu. A pressed row adds `bg-dashboard-fill-strong`.
71
90
  - For a link that looks like a button, put `buttonVariants()` on the router
72
91
  `Link`. A form page has one `Button variant="primary"` for its main save
73
92
  action.
@@ -134,14 +153,20 @@ Shared class strings live in `src/client/styles.ts`.
134
153
  child. Use `ui/accordion` for a set of sections with headings.
135
154
  - **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
136
155
  `BottomSheet` below that width. It owns Escape and the scroll frame for
137
- docked panels.
156
+ docked panels. Docked actions sit beside the close control, so their
157
+ position does not change with the title width.
158
+ - **Drawer.** `Drawer` is a right-side `ui/drawer`. A `Drawer` opened from
159
+ inside another `Drawer` stacks in front of it. The parent scales back,
160
+ shows a thin edge behind the child, and hides its content. Set `flush` when
161
+ the content owns its insets and sticky headers, such as the event log.
138
162
  - **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
139
163
  Deployment, and Item icons. Do not replace it with a provider logo or a
140
164
  status-only icon.
141
165
  - **Loading.** Use `ui/skeleton` for placeholder leaves and `PageContentSkeleton`
142
166
  variants for page bodies. Use `PageRouteLoading` when a page keeps its real
143
167
  header above a body skeleton. Use a feature-owned loading component for
144
- distinct geometry, such as conversations.
168
+ distinct geometry, such as conversations. Use `ui/spinner` for an inline
169
+ pending icon.
145
170
 
146
171
  ## Navigation
147
172
 
@@ -2,6 +2,12 @@ import type { Ref } from "react";
2
2
  import { Search, X } from "lucide-react";
3
3
 
4
4
  import { cn } from "../styles";
5
+ import {
6
+ InputGroup,
7
+ InputGroupAddon,
8
+ InputGroupButton,
9
+ InputGroupInput,
10
+ } from "./ui/input-group";
5
11
 
6
12
  type SearchInputSize = "compact" | "default";
7
13
 
@@ -15,23 +21,14 @@ export function SearchInput(props: {
15
21
  size?: SearchInputSize;
16
22
  value: string;
17
23
  }) {
18
- const size = props.size ?? "compact";
24
+ const size = sizeClass[props.size ?? "compact"];
19
25
  return (
20
- <div className={cn("relative min-w-0", props.className)}>
21
- <Search
22
- aria-hidden="true"
23
- className={cn(
24
- "pointer-events-none absolute top-1/2 -translate-y-1/2 text-dashboard-text-muted",
25
- searchIconClass[size],
26
- )}
27
- size={size === "compact" ? 13 : 14}
28
- strokeWidth={2.5}
29
- />
30
- <input
26
+ <InputGroup className={cn(size.group, props.className)}>
27
+ <InputGroupInput
31
28
  aria-label={props.label}
32
29
  className={cn(
33
- "w-full rounded-md border border-dashboard-border font-sans text-sm text-dashboard-text outline-none transition-colors placeholder:text-dashboard-text-muted/70 hover:border-dashboard-border-emphasis focus:border-dashboard-focus [&::-webkit-search-cancel-button]:appearance-none",
34
- searchInputClass[size],
30
+ "[&::-webkit-search-cancel-button]:appearance-none",
31
+ size.input,
35
32
  )}
36
33
  onChange={(event) => props.onChange(event.currentTarget.value)}
37
34
  placeholder={props.placeholder}
@@ -39,40 +36,53 @@ export function SearchInput(props: {
39
36
  type="search"
40
37
  value={props.value}
41
38
  />
42
- {props.value ? (
43
- <button
39
+ <InputGroupAddon className={size.icon}>
40
+ <Search aria-hidden="true" size={size.iconSize} strokeWidth={2.5} />
41
+ </InputGroupAddon>
42
+ {/* The clear button keeps its space while hidden, so the field width does not change. */}
43
+ <InputGroupAddon align="inline-end" className={size.clear}>
44
+ <InputGroupButton
44
45
  aria-label="Clear search"
45
- className={cn(
46
- "absolute top-1/2 grid size-6 -translate-y-1/2 cursor-pointer place-items-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
47
- clearButtonClass[size],
48
- )}
46
+ className={props.value ? undefined : "invisible"}
49
47
  onClick={(event) => {
50
48
  props.onChange("");
51
- // The button unmounts once the value is empty, so keep focus in the search box.
52
- event.currentTarget.parentElement?.querySelector("input")?.focus();
49
+ // The button hides once the value is empty, so keep focus in the search box.
50
+ event.currentTarget
51
+ .closest("[data-slot=input-group]")
52
+ ?.querySelector("input")
53
+ ?.focus();
53
54
  }}
54
- type="button"
55
55
  >
56
56
  <X aria-hidden="true" size={14} strokeWidth={2.5} />
57
- </button>
58
- ) : null}
59
- </div>
57
+ </InputGroupButton>
58
+ </InputGroupAddon>
59
+ </InputGroup>
60
60
  );
61
61
  }
62
62
 
63
- const searchIconClass: Record<SearchInputSize, string> = {
64
- compact: "left-2.5",
65
- default: "left-3",
66
- };
67
-
68
- const clearButtonClass: Record<SearchInputSize, string> = {
69
- compact: "right-1.5",
70
- default: "right-2",
71
- };
72
-
73
- const searchInputClass: Record<SearchInputSize, string> = {
74
- compact:
75
- "h-9 bg-dashboard-overlay-soft pr-8 pl-8 focus:ring-1 focus:ring-dashboard-focus/25",
76
- default:
77
- "h-10 bg-dashboard-overlay-soft pr-9 pl-9 focus:ring-1 focus:ring-dashboard-focus/25",
63
+ // Match the icon, text, and clear button offsets of the bordered input.
64
+ const sizeClass: Record<
65
+ SearchInputSize,
66
+ {
67
+ clear: string;
68
+ group: string;
69
+ icon: string;
70
+ iconSize: number;
71
+ input: string;
72
+ }
73
+ > = {
74
+ compact: {
75
+ clear: "pr-1.25",
76
+ group: "h-9",
77
+ icon: "pl-2.25",
78
+ iconSize: 13,
79
+ input: "pr-1 pl-2.5",
80
+ },
81
+ default: {
82
+ clear: "pr-1.75",
83
+ group: "h-10",
84
+ icon: "pl-2.75",
85
+ iconSize: 14,
86
+ input: "pr-1 pl-2.75",
87
+ },
78
88
  };
@@ -1,24 +1,26 @@
1
1
  import { JUNIOR_VERSION } from "@sentry/junior/version";
2
2
 
3
+ import { Alert } from "./ui/alert";
4
+ import { Button } from "./ui/button";
5
+
3
6
  /** Prompt the viewer to load the dashboard bundle that matches the server. */
4
7
  export function VersionDriftBanner(props: { serverVersion?: string }) {
5
8
  if (!dashboardVersionDrift(props.serverVersion)) return null;
6
9
 
7
10
  return (
8
- <div
11
+ <Alert
9
12
  aria-live="polite"
10
- className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
13
+ className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 rounded-none border-0 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
11
14
  role="status"
12
15
  >
13
16
  <span>The dashboard changed. Refresh to load the latest version.</span>
14
- <button
15
- className="cursor-pointer border-0 bg-transparent p-0 font-sans font-semibold text-dashboard-accent-text underline decoration-dashboard-accent/40 underline-offset-2 hover:text-dashboard-text-solid"
17
+ <Button
18
+ className="h-auto border-0 bg-transparent p-0 text-xs font-semibold leading-normal text-dashboard-accent-text underline decoration-dashboard-accent/40 underline-offset-2 hover:bg-transparent hover:text-dashboard-text-solid"
16
19
  onClick={() => window.location.reload()}
17
- type="button"
18
20
  >
19
21
  Refresh
20
- </button>
21
- </div>
22
+ </Button>
23
+ </Alert>
22
24
  );
23
25
  }
24
26
 
@@ -9,6 +9,14 @@ import {
9
9
  CollapsibleContent,
10
10
  CollapsibleTrigger,
11
11
  } from "../ui/collapsible";
12
+ import {
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from "../ui/table";
12
20
  import { dashboardDisclosureChevronClass } from "../../styles";
13
21
 
14
22
  /** Identify the largest cache writes within the page's selected time window. */
@@ -40,50 +48,51 @@ export function CacheWrites(props: {
40
48
  Top three by tokens written per {unit}, within the selected time
41
49
  window. Large writes alone do not mean a cache problem.
42
50
  </p>
43
- <div className="overflow-x-auto">
44
- <table
45
- aria-label="Largest cache writes"
46
- className="w-full min-w-80 table-fixed border-collapse font-sans text-xs tabular-nums"
47
- >
48
- <thead className="text-dashboard-text-muted">
49
- <tr>
50
- <th scope="col" className="py-2 text-left font-normal">
51
- {props.bucketUnit === "day" ? "Day" : "Starting at"}
52
- </th>
53
- <th scope="col" className="w-24 py-2 text-right font-normal">
54
- Written
55
- </th>
56
- <th scope="col" className="w-24 py-2 text-right font-normal">
57
- From cache
58
- </th>
59
- </tr>
60
- </thead>
61
- <tbody>
62
- {ranked.map((day) => (
63
- <tr
64
- key={day.date}
65
- className="border-t border-dashboard-border-subtle"
51
+ <Table
52
+ aria-label="Largest cache writes"
53
+ className="min-w-80 table-fixed font-sans text-xs tabular-nums"
54
+ >
55
+ <TableHeader>
56
+ <TableRow>
57
+ <TableHead scope="col" className="px-0 py-2 font-normal">
58
+ {props.bucketUnit === "day" ? "Day" : "Starting at"}
59
+ </TableHead>
60
+ <TableHead
61
+ scope="col"
62
+ className="w-24 px-0 py-2 text-right font-normal"
63
+ >
64
+ Written
65
+ </TableHead>
66
+ <TableHead
67
+ scope="col"
68
+ className="w-24 px-0 py-2 text-right font-normal"
69
+ >
70
+ From cache
71
+ </TableHead>
72
+ </TableRow>
73
+ </TableHeader>
74
+ <TableBody>
75
+ {ranked.map((day) => (
76
+ <TableRow key={day.date}>
77
+ <TableHead
78
+ scope="row"
79
+ className="px-0 py-2 font-normal whitespace-normal"
66
80
  >
67
- <th
68
- scope="row"
69
- className="py-2 text-left font-normal text-dashboard-text-muted"
70
- >
71
- {formatActivityTooltipDate(day.date)}
72
- </th>
73
- <td className="py-2 text-right text-dashboard-text">
74
- {formatCompactNumber(day.cacheCreationTokens)}
75
- </td>
76
- <td className="py-2 text-right text-dashboard-text">
77
- {formatCacheShare(
78
- day.cachedInputTokens,
79
- cacheInputTotal(day),
80
- )}
81
- </td>
82
- </tr>
83
- ))}
84
- </tbody>
85
- </table>
86
- </div>
81
+ {formatActivityTooltipDate(day.date)}
82
+ </TableHead>
83
+ <TableCell className="px-0 py-2 text-right whitespace-normal">
84
+ {formatCompactNumber(day.cacheCreationTokens)}
85
+ </TableCell>
86
+ <TableCell className="px-0 py-2 text-right whitespace-normal">
87
+ {formatCacheShare(
88
+ day.cachedInputTokens,
89
+ cacheInputTotal(day),
90
+ )}
91
+ </TableCell>
92
+ </TableRow>
93
+ ))}
94
+ </TableBody>
95
+ </Table>
87
96
  </CollapsibleContent>
88
97
  </Collapsible>
89
98
  );