@sentry/junior-dashboard 0.240.0 → 0.241.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.
@@ -34,9 +34,11 @@ import { cn } from "../../styles";
34
34
  import {
35
35
  type MemoryDashboardData,
36
36
  useMemoryDashboardData,
37
+ useMemorySearchCounts,
37
38
  } from "./memoryDashboard";
38
39
  import { MemoryDetailsDrawer } from "./MemoryDetailsDrawer";
39
40
  import { MemoryPageLayout } from "./MemoryPageLayout";
41
+ import { MemorySearchHighlight } from "./MemorySearchHighlight";
40
42
  import { MemoryTimeline } from "./MemoryTimeline";
41
43
  import { MemoryCostChart } from "./MemoryCostChart";
42
44
  import { useMemoryRecord } from "./memoryRecord";
@@ -151,6 +153,11 @@ function MemoryLibrary(props: {
151
153
  setSearchText,
152
154
  } = usePluginUserPageData(props.page);
153
155
  const dashboardQuery = useMemoryDashboardData();
156
+ const searchCountsQuery = useMemorySearchCounts(searchQuery);
157
+ // Tab totals follow the search. Without a search, the summary owns them.
158
+ const counts = searchQuery
159
+ ? searchCountsQuery.data
160
+ : dashboardQuery.data?.stats;
154
161
  const navigate = useNavigate();
155
162
  const location = useLocation();
156
163
  const { memoryId } = useParams();
@@ -217,17 +224,17 @@ function MemoryLibrary(props: {
217
224
  ariaLabel="Memory collections"
218
225
  items={[
219
226
  {
220
- count: dashboardQuery.data?.stats?.active,
227
+ count: counts?.active,
221
228
  label: "All",
222
229
  value: "",
223
230
  },
224
231
  {
225
- count: dashboardQuery.data?.stats?.personal,
232
+ count: counts?.personal,
226
233
  label: "Private",
227
234
  value: "private",
228
235
  },
229
236
  {
230
- count: dashboardQuery.data?.stats?.public,
237
+ count: counts?.public,
231
238
  label: "Public",
232
239
  value: "public",
233
240
  },
@@ -274,6 +281,7 @@ function MemoryLibrary(props: {
274
281
  )
275
282
  }
276
283
  record={record}
284
+ searchQuery={searchQuery}
277
285
  selected={record.id === memoryId}
278
286
  />
279
287
  ))}
@@ -472,6 +480,7 @@ function MemoryRow(props: {
472
480
  first: boolean;
473
481
  onSelect(): void;
474
482
  record: PluginUserPageRecord;
483
+ searchQuery: string;
475
484
  selected: boolean;
476
485
  }) {
477
486
  const kind = metadataValue(props.record, "Type");
@@ -506,7 +515,10 @@ function MemoryRow(props: {
506
515
  size={14}
507
516
  />
508
517
  <h3 className="m-0 truncate font-display text-base font-medium leading-snug text-dashboard-text">
509
- {props.record.title}
518
+ <MemorySearchHighlight
519
+ query={props.searchQuery}
520
+ text={props.record.title}
521
+ />
510
522
  </h3>
511
523
  </div>
512
524
  <div className="mt-1.5 truncate font-mono text-xs leading-relaxed text-dashboard-text-muted">
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Split a search query into the terms that memory search matches. Keep this
3
+ * in step with `searchTerms` in `@sentry/junior-memory` `src/viewer.ts`.
4
+ */
5
+ function memorySearchTerms(query: string): string[] {
6
+ return [
7
+ ...new Set(
8
+ query
9
+ .toLowerCase()
10
+ .split(/[^a-z0-9_'-]+/)
11
+ .filter((term) => term.length >= 2),
12
+ ),
13
+ ];
14
+ }
15
+
16
+ function escapeRegExp(value: string): string {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+
20
+ /** Render memory text with each matched search term marked. */
21
+ export function MemorySearchHighlight(props: { query: string; text: string }) {
22
+ const terms = memorySearchTerms(props.query);
23
+ if (terms.length === 0) return <>{props.text}</>;
24
+ // Longer terms first so a term that contains another term wins the match.
25
+ const pattern = new RegExp(
26
+ `(${terms
27
+ .sort((a, b) => b.length - a.length)
28
+ .map(escapeRegExp)
29
+ .join("|")})`,
30
+ "gi",
31
+ );
32
+ // `split` with one capture group puts the matches at odd indexes.
33
+ return (
34
+ <>
35
+ {props.text.split(pattern).map((part, index) =>
36
+ index % 2 === 1 ? (
37
+ <mark
38
+ className="rounded-sm bg-amber-300/25 text-dashboard-text"
39
+ key={index}
40
+ >
41
+ {part}
42
+ </mark>
43
+ ) : (
44
+ part
45
+ ),
46
+ )}
47
+ </>
48
+ );
49
+ }
@@ -1,4 +1,4 @@
1
- import { useQuery } from "@tanstack/react-query";
1
+ import { keepPreviousData, useQuery } from "@tanstack/react-query";
2
2
  import { z } from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
@@ -64,3 +64,36 @@ export function useMemoryDashboardData() {
64
64
  retry: false,
65
65
  });
66
66
  }
67
+
68
+ const memoryCountsSchema = z
69
+ .object({
70
+ active: z.number().int().min(0),
71
+ personal: z.number().int().min(0),
72
+ public: z.number().int().min(0),
73
+ })
74
+ .strict();
75
+
76
+ /**
77
+ * Load viewer-scoped memory totals that match a search query. The query stays
78
+ * idle without a search, because the dashboard summary owns unfiltered totals.
79
+ */
80
+ export function useMemorySearchCounts(searchQuery: string) {
81
+ return useQuery({
82
+ enabled: Boolean(searchQuery),
83
+ placeholderData: keepPreviousData,
84
+ queryFn: ({ signal }) =>
85
+ fetchDashboardJson(
86
+ memoryCountsSchema,
87
+ `/api/plugins/memory/counts?${new URLSearchParams({ q: searchQuery })}`,
88
+ signal,
89
+ ),
90
+ queryKey: [
91
+ "dashboard",
92
+ "plugin-user-page",
93
+ "memory",
94
+ "counts",
95
+ searchQuery,
96
+ ],
97
+ retry: false,
98
+ });
99
+ }