@vectara/vectara-ui 16.3.1 → 16.4.1

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.
@@ -0,0 +1,388 @@
1
+ import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
2
+ const getId = (s) => s.id;
3
+ const getParentId = (s) => s.parentId;
4
+ const flatten = (rows, expandedIds = new Set()) => buildAndFlattenSpans(rows, expandedIds, getId, getParentId);
5
+ describe("buildAndFlattenSpans", () => {
6
+ test("emits only top-level rows when nothing is expanded", () => {
7
+ const rows = [
8
+ { id: "a", parentId: null },
9
+ { id: "b", parentId: null },
10
+ { id: "a-1", parentId: "a" },
11
+ { id: "a-2", parentId: "a" }
12
+ ];
13
+ expect(flatten(rows)).toEqual([
14
+ {
15
+ row: rows[0],
16
+ id: "a",
17
+ parentId: null,
18
+ depth: 0,
19
+ hasChildren: true,
20
+ hasLoadedChildren: true,
21
+ posInSet: 1,
22
+ setSize: 2
23
+ },
24
+ {
25
+ row: rows[1],
26
+ id: "b",
27
+ parentId: null,
28
+ depth: 0,
29
+ hasChildren: false,
30
+ hasLoadedChildren: false,
31
+ posInSet: 2,
32
+ setSize: 2
33
+ }
34
+ ]);
35
+ });
36
+ test("emits children of expanded parents in render order", () => {
37
+ const rows = [
38
+ { id: "a", parentId: null },
39
+ { id: "b", parentId: null },
40
+ { id: "a-1", parentId: "a" },
41
+ { id: "a-2", parentId: "a" }
42
+ ];
43
+ expect(flatten(rows, new Set(["a"]))).toEqual([
44
+ {
45
+ row: rows[0],
46
+ id: "a",
47
+ parentId: null,
48
+ depth: 0,
49
+ hasChildren: true,
50
+ hasLoadedChildren: true,
51
+ posInSet: 1,
52
+ setSize: 2
53
+ },
54
+ {
55
+ row: rows[2],
56
+ id: "a-1",
57
+ parentId: "a",
58
+ depth: 1,
59
+ hasChildren: false,
60
+ hasLoadedChildren: false,
61
+ posInSet: 1,
62
+ setSize: 2
63
+ },
64
+ {
65
+ row: rows[3],
66
+ id: "a-2",
67
+ parentId: "a",
68
+ depth: 1,
69
+ hasChildren: false,
70
+ hasLoadedChildren: false,
71
+ posInSet: 2,
72
+ setSize: 2
73
+ },
74
+ {
75
+ row: rows[1],
76
+ id: "b",
77
+ parentId: null,
78
+ depth: 0,
79
+ hasChildren: false,
80
+ hasLoadedChildren: false,
81
+ posInSet: 2,
82
+ setSize: 2
83
+ }
84
+ ]);
85
+ });
86
+ test("walks deep trees", () => {
87
+ const rows = [
88
+ { id: "a", parentId: null },
89
+ { id: "a-1", parentId: "a" },
90
+ { id: "a-1-1", parentId: "a-1" },
91
+ { id: "a-1-1-1", parentId: "a-1-1" }
92
+ ];
93
+ expect(flatten(rows, new Set(["a", "a-1", "a-1-1"]))).toEqual([
94
+ {
95
+ row: rows[0],
96
+ id: "a",
97
+ parentId: null,
98
+ depth: 0,
99
+ hasChildren: true,
100
+ hasLoadedChildren: true,
101
+ posInSet: 1,
102
+ setSize: 1
103
+ },
104
+ {
105
+ row: rows[1],
106
+ id: "a-1",
107
+ parentId: "a",
108
+ depth: 1,
109
+ hasChildren: true,
110
+ hasLoadedChildren: true,
111
+ posInSet: 1,
112
+ setSize: 1
113
+ },
114
+ {
115
+ row: rows[2],
116
+ id: "a-1-1",
117
+ parentId: "a-1",
118
+ depth: 2,
119
+ hasChildren: true,
120
+ hasLoadedChildren: true,
121
+ posInSet: 1,
122
+ setSize: 1
123
+ },
124
+ {
125
+ row: rows[3],
126
+ id: "a-1-1-1",
127
+ parentId: "a-1-1",
128
+ depth: 3,
129
+ hasChildren: false,
130
+ hasLoadedChildren: false,
131
+ posInSet: 1,
132
+ setSize: 1
133
+ }
134
+ ]);
135
+ });
136
+ test("flags hasChildren when actual children exist", () => {
137
+ const rows = [
138
+ { id: "a", parentId: null },
139
+ { id: "b", parentId: null },
140
+ { id: "a-1", parentId: "a" }
141
+ ];
142
+ expect(flatten(rows)).toEqual([
143
+ {
144
+ row: rows[0],
145
+ id: "a",
146
+ parentId: null,
147
+ depth: 0,
148
+ hasChildren: true,
149
+ hasLoadedChildren: true,
150
+ posInSet: 1,
151
+ setSize: 2
152
+ },
153
+ {
154
+ row: rows[1],
155
+ id: "b",
156
+ parentId: null,
157
+ depth: 0,
158
+ hasChildren: false,
159
+ hasLoadedChildren: false,
160
+ posInSet: 2,
161
+ setSize: 2
162
+ }
163
+ ]);
164
+ });
165
+ test("flags hasChildren when row says so even with no actual children", () => {
166
+ const rows = [{ id: "a", parentId: null, hasChildren: true }];
167
+ expect(flatten(rows)).toEqual([
168
+ {
169
+ row: rows[0],
170
+ id: "a",
171
+ parentId: null,
172
+ depth: 0,
173
+ hasChildren: true,
174
+ hasLoadedChildren: false,
175
+ posInSet: 1,
176
+ setSize: 1
177
+ }
178
+ ]);
179
+ });
180
+ test("does not recurse when expanded but no children loaded", () => {
181
+ // Lazy-load case: row says it has children but they aren't in the list.
182
+ const rows = [{ id: "a", parentId: null, hasChildren: true }];
183
+ expect(flatten(rows, new Set(["a"]))).toEqual([
184
+ {
185
+ row: rows[0],
186
+ id: "a",
187
+ parentId: null,
188
+ depth: 0,
189
+ hasChildren: true,
190
+ hasLoadedChildren: false,
191
+ posInSet: 1,
192
+ setSize: 1
193
+ }
194
+ ]);
195
+ });
196
+ test("drops orphans (parent id that doesn't appear in rows)", () => {
197
+ const rows = [
198
+ { id: "a", parentId: null },
199
+ { id: "ghost", parentId: "missing" }
200
+ ];
201
+ expect(flatten(rows)).toEqual([
202
+ {
203
+ row: rows[0],
204
+ id: "a",
205
+ parentId: null,
206
+ depth: 0,
207
+ hasChildren: false,
208
+ hasLoadedChildren: false,
209
+ posInSet: 1,
210
+ setSize: 1
211
+ }
212
+ ]);
213
+ });
214
+ test("breaks cycles", () => {
215
+ // Duplicate id forms a cycle: the root "a" claims "a" as a child of itself.
216
+ // The recursive walk should bail at the duplicate, leaving just the root.
217
+ const rows = [
218
+ { id: "a", parentId: null },
219
+ { id: "a", parentId: "a" }
220
+ ];
221
+ expect(flatten(rows, new Set(["a"]))).toEqual([
222
+ {
223
+ row: rows[0],
224
+ id: "a",
225
+ parentId: null,
226
+ depth: 0,
227
+ hasChildren: true,
228
+ hasLoadedChildren: true,
229
+ posInSet: 1,
230
+ setSize: 1
231
+ }
232
+ ]);
233
+ });
234
+ test("sets posInSet and setSize per sibling group", () => {
235
+ const rows = [
236
+ { id: "a", parentId: null },
237
+ { id: "b", parentId: null },
238
+ { id: "c", parentId: null },
239
+ { id: "b-1", parentId: "b" },
240
+ { id: "b-2", parentId: "b" }
241
+ ];
242
+ expect(flatten(rows, new Set(["b"]))).toEqual([
243
+ {
244
+ row: rows[0],
245
+ id: "a",
246
+ parentId: null,
247
+ depth: 0,
248
+ hasChildren: false,
249
+ hasLoadedChildren: false,
250
+ posInSet: 1,
251
+ setSize: 3
252
+ },
253
+ {
254
+ row: rows[1],
255
+ id: "b",
256
+ parentId: null,
257
+ depth: 0,
258
+ hasChildren: true,
259
+ hasLoadedChildren: true,
260
+ posInSet: 2,
261
+ setSize: 3
262
+ },
263
+ {
264
+ row: rows[3],
265
+ id: "b-1",
266
+ parentId: "b",
267
+ depth: 1,
268
+ hasChildren: false,
269
+ hasLoadedChildren: false,
270
+ posInSet: 1,
271
+ setSize: 2
272
+ },
273
+ {
274
+ row: rows[4],
275
+ id: "b-2",
276
+ parentId: "b",
277
+ depth: 1,
278
+ hasChildren: false,
279
+ hasLoadedChildren: false,
280
+ posInSet: 2,
281
+ setSize: 2
282
+ },
283
+ {
284
+ row: rows[2],
285
+ id: "c",
286
+ parentId: null,
287
+ depth: 0,
288
+ hasChildren: false,
289
+ hasLoadedChildren: false,
290
+ posInSet: 3,
291
+ setSize: 3
292
+ }
293
+ ]);
294
+ });
295
+ test("preserves input order among siblings", () => {
296
+ const rows = [
297
+ { id: "z", parentId: null },
298
+ { id: "a", parentId: null },
299
+ { id: "m", parentId: null }
300
+ ];
301
+ expect(flatten(rows)).toEqual([
302
+ {
303
+ row: rows[0],
304
+ id: "z",
305
+ parentId: null,
306
+ depth: 0,
307
+ hasChildren: false,
308
+ hasLoadedChildren: false,
309
+ posInSet: 1,
310
+ setSize: 3
311
+ },
312
+ {
313
+ row: rows[1],
314
+ id: "a",
315
+ parentId: null,
316
+ depth: 0,
317
+ hasChildren: false,
318
+ hasLoadedChildren: false,
319
+ posInSet: 2,
320
+ setSize: 3
321
+ },
322
+ {
323
+ row: rows[2],
324
+ id: "m",
325
+ parentId: null,
326
+ depth: 0,
327
+ hasChildren: false,
328
+ hasLoadedChildren: false,
329
+ posInSet: 3,
330
+ setSize: 3
331
+ }
332
+ ]);
333
+ });
334
+ test("collapsed parent hides its descendants but state persists for re-expand", () => {
335
+ const rows = [
336
+ { id: "a", parentId: null },
337
+ { id: "a-1", parentId: "a" },
338
+ { id: "a-1-1", parentId: "a-1" }
339
+ ];
340
+ // Both a and a-1 expanded — full subtree visible.
341
+ expect(flatten(rows, new Set(["a", "a-1"]))).toEqual([
342
+ {
343
+ row: rows[0],
344
+ id: "a",
345
+ parentId: null,
346
+ depth: 0,
347
+ hasChildren: true,
348
+ hasLoadedChildren: true,
349
+ posInSet: 1,
350
+ setSize: 1
351
+ },
352
+ {
353
+ row: rows[1],
354
+ id: "a-1",
355
+ parentId: "a",
356
+ depth: 1,
357
+ hasChildren: true,
358
+ hasLoadedChildren: true,
359
+ posInSet: 1,
360
+ setSize: 1
361
+ },
362
+ {
363
+ row: rows[2],
364
+ id: "a-1-1",
365
+ parentId: "a-1",
366
+ depth: 2,
367
+ hasChildren: false,
368
+ hasLoadedChildren: false,
369
+ posInSet: 1,
370
+ setSize: 1
371
+ }
372
+ ]);
373
+ // a-1 still in expandedIds but a is collapsed — only a should render. The
374
+ // a-1 expand state is preserved for when the user re-expands a.
375
+ expect(flatten(rows, new Set(["a-1"]))).toEqual([
376
+ {
377
+ row: rows[0],
378
+ id: "a",
379
+ parentId: null,
380
+ depth: 0,
381
+ hasChildren: true,
382
+ hasLoadedChildren: true,
383
+ posInSet: 1,
384
+ setSize: 1
385
+ }
386
+ ]);
387
+ });
388
+ });
@@ -0,0 +1,5 @@
1
+ import { Column, Row } from "../table/types";
2
+ export type { Column };
3
+ export type SpansRow = Row & {
4
+ hasChildren?: boolean;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -4786,6 +4786,104 @@ h2.react-datepicker__current-month {
4786
4786
  height: 40px;
4787
4787
  }
4788
4788
 
4789
+ .vuiSpansWrapper {
4790
+ width: 100%;
4791
+ position: relative;
4792
+ }
4793
+
4794
+ .vuiSpans {
4795
+ width: 100%;
4796
+ table-layout: fixed;
4797
+ }
4798
+ .vuiSpans thead {
4799
+ border-bottom: 1px solid var(--vui-color-border-medium);
4800
+ }
4801
+ .vuiSpans tbody tr {
4802
+ border-bottom: 1px solid var(--vui-color-border-light);
4803
+ }
4804
+ .vuiSpans tbody tr:not(.vuiSpansRow--inert):hover {
4805
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.25);
4806
+ }
4807
+ .vuiSpans tbody tr:last-child {
4808
+ border-bottom: 1px solid var(--vui-color-border-medium);
4809
+ }
4810
+ .vuiSpans th {
4811
+ font-size: 14px;
4812
+ font-weight: 600;
4813
+ padding: 4px;
4814
+ text-align: left;
4815
+ }
4816
+ .vuiSpans td {
4817
+ font-size: 14px;
4818
+ padding: 4px;
4819
+ vertical-align: middle;
4820
+ word-break: break-word;
4821
+ }
4822
+
4823
+ .vuiSpans--fluid {
4824
+ table-layout: auto;
4825
+ }
4826
+
4827
+ .vuiSpansCell__indent {
4828
+ display: flex;
4829
+ align-items: center;
4830
+ gap: 4px;
4831
+ }
4832
+
4833
+ .vuiSpansCell__chevron {
4834
+ flex: 0 0 auto;
4835
+ width: 24px;
4836
+ display: flex;
4837
+ align-items: center;
4838
+ justify-content: center;
4839
+ }
4840
+
4841
+ .vuiSpansCell__chevronPlaceholder {
4842
+ display: inline-block;
4843
+ width: 24px;
4844
+ height: 1px;
4845
+ }
4846
+
4847
+ .vuiSpansCell {
4848
+ display: flex;
4849
+ align-items: center;
4850
+ min-width: 0;
4851
+ flex: 1 1 auto;
4852
+ }
4853
+
4854
+ .vuiSpansCellWrapper--truncate {
4855
+ min-width: 0;
4856
+ overflow: hidden;
4857
+ white-space: nowrap;
4858
+ text-overflow: ellipsis;
4859
+ }
4860
+
4861
+ .vuiSpansHeaderCell {
4862
+ display: flex;
4863
+ align-items: center;
4864
+ padding: 4px;
4865
+ }
4866
+
4867
+ .vuiSpansStickyHeader {
4868
+ position: sticky;
4869
+ top: 0;
4870
+ background-color: var(--vui-color-empty-shade);
4871
+ z-index: 1;
4872
+ }
4873
+
4874
+ .vuiSpansLoadingRow {
4875
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.15);
4876
+ }
4877
+
4878
+ .vuiSpansLoadingRow__cell {
4879
+ padding: 4px !important;
4880
+ }
4881
+
4882
+ .vuiSpansLoadingRow__inner {
4883
+ display: flex;
4884
+ align-items: center;
4885
+ }
4886
+
4789
4887
  .vuiSpinner {
4790
4888
  display: inline-block;
4791
4889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectara/vectara-ui",
3
- "version": "16.3.1",
3
+ "version": "16.4.1",
4
4
  "homepage": "./",
5
5
  "description": "Vectara's design system, codified as a React and Sass component library",
6
6
  "author": "Vectara",
@@ -218,6 +218,20 @@ export const PrimaryDrawer = () => {
218
218
  that the focus management and scroll locking still work as expected.
219
219
  </p>
220
220
  </VuiText>
221
+
222
+ <VuiSpacer size="m" />
223
+
224
+ <VuiButtonSecondary
225
+ color="primary"
226
+ onClick={() => {
227
+ addNotification({
228
+ color: "primary",
229
+ message: "Just some information, FYI"
230
+ });
231
+ }}
232
+ >
233
+ Add notification
234
+ </VuiButtonSecondary>
221
235
  </VuiModal>
222
236
 
223
237
  <VuiNotifications />
@@ -0,0 +1,170 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ VuiBadge,
4
+ VuiFlexContainer,
5
+ VuiFlexItem,
6
+ VuiIcon,
7
+ VuiSpacer,
8
+ VuiSpans,
9
+ VuiText,
10
+ VuiTextColor,
11
+ VuiToggle
12
+ } from "../../../lib";
13
+ import { BiCog, BiError, BiNetworkChart, BiSearch, BiSitemap, BiSpreadsheet } from "react-icons/bi";
14
+ import { FakeSpan, LAZY_CHILDREN, ROOT_SPANS } from "./createFakeSpans";
15
+
16
+ const KIND_ICON = {
17
+ workflow: BiSitemap,
18
+ tool: BiCog,
19
+ llm: BiNetworkChart,
20
+ search: BiSearch,
21
+ embedding: BiSpreadsheet
22
+ } as const;
23
+
24
+ const STATUS_COLOR = {
25
+ ok: "success",
26
+ error: "danger",
27
+ running: "primary"
28
+ } as const;
29
+
30
+ export const Spans = () => {
31
+ const [hasData, setHasData] = useState(true);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [hasError, setHasError] = useState(false);
34
+ const [isHeaderSticky, setIsHeaderSticky] = useState(false);
35
+ const [fluid, setFluid] = useState(true);
36
+
37
+ const [rows, setRows] = useState<FakeSpan[]>(ROOT_SPANS);
38
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(["wf-1"]));
39
+
40
+ const visibleRows = hasData ? rows : [];
41
+
42
+ const onExpand = async (row: FakeSpan) => {
43
+ // Only the wf-1-c2 span lazy-loads in this demo.
44
+ if (row.id !== "wf-1-c2") return;
45
+
46
+ // Simulate a network round-trip.
47
+ await new Promise((resolve) => setTimeout(resolve, 1200));
48
+
49
+ setRows((prev) => {
50
+ // De-dupe in case the user toggles repeatedly.
51
+ const alreadyLoaded = prev.some((r) => r.parentId === row.id);
52
+ if (alreadyLoaded) return prev;
53
+ return prev.concat(LAZY_CHILDREN);
54
+ });
55
+ };
56
+
57
+ const columns = useMemo(
58
+ () => [
59
+ {
60
+ name: "name",
61
+ width: "55%",
62
+ header: { render: () => "Name" },
63
+ render: (row: FakeSpan) => {
64
+ const KindIcon = KIND_ICON[row.kind];
65
+ return (
66
+ <VuiFlexContainer alignItems="center" spacing="xs">
67
+ <VuiFlexItem grow={false} shrink={false}>
68
+ <VuiIcon size="s" color="subdued">
69
+ <KindIcon />
70
+ </VuiIcon>
71
+ </VuiFlexItem>
72
+ <VuiFlexItem grow={false}>{row.name}</VuiFlexItem>
73
+ </VuiFlexContainer>
74
+ );
75
+ }
76
+ },
77
+ {
78
+ name: "status",
79
+ width: "120px",
80
+ header: { render: () => "Status" },
81
+ render: (row: FakeSpan) => <VuiBadge color={STATUS_COLOR[row.status]}>{row.status}</VuiBadge>
82
+ },
83
+ {
84
+ name: "startAt",
85
+ width: "140px",
86
+ header: { render: () => "Start at" },
87
+ render: (row: FakeSpan) => (
88
+ <VuiText size="s">
89
+ <p>
90
+ <VuiTextColor color="subdued">{row.startAt}</VuiTextColor>
91
+ </p>
92
+ </VuiText>
93
+ )
94
+ },
95
+ {
96
+ name: "duration",
97
+ width: "120px",
98
+ header: { render: () => "Duration" },
99
+ render: (row: FakeSpan) => (
100
+ <VuiText size="s">
101
+ <p>
102
+ <VuiTextColor color="subdued">{row.durationMs}ms</VuiTextColor>
103
+ </p>
104
+ </VuiText>
105
+ )
106
+ }
107
+ ],
108
+ []
109
+ );
110
+
111
+ const errorContent = (
112
+ <>
113
+ <VuiFlexItem grow={false}>
114
+ <VuiIcon color="danger">
115
+ <BiError />
116
+ </VuiIcon>
117
+ </VuiFlexItem>
118
+ <VuiFlexItem grow={false}>
119
+ <VuiText>
120
+ <p>
121
+ <VuiTextColor color="danger">Couldn't retrieve trace</VuiTextColor>
122
+ </p>
123
+ </VuiText>
124
+ </VuiFlexItem>
125
+ </>
126
+ );
127
+
128
+ return (
129
+ <>
130
+ <VuiFlexContainer wrap spacing="l">
131
+ <VuiFlexItem shrink={false}>
132
+ <VuiToggle label="Has data" checked={hasData} onChange={(e) => setHasData(e.target.checked)} />
133
+ </VuiFlexItem>
134
+ <VuiFlexItem shrink={false}>
135
+ <VuiToggle label="Is loading" checked={isLoading} onChange={(e) => setIsLoading(e.target.checked)} />
136
+ </VuiFlexItem>
137
+ <VuiFlexItem shrink={false}>
138
+ <VuiToggle label="Has error" checked={hasError} onChange={(e) => setHasError(e.target.checked)} />
139
+ </VuiFlexItem>
140
+ <VuiFlexItem shrink={false}>
141
+ <VuiToggle
142
+ label="Sticky header"
143
+ checked={isHeaderSticky}
144
+ onChange={(e) => setIsHeaderSticky(e.target.checked)}
145
+ />
146
+ </VuiFlexItem>
147
+ <VuiFlexItem shrink={false}>
148
+ <VuiToggle label="Fluid layout" checked={fluid} onChange={(e) => setFluid(e.target.checked)} />
149
+ </VuiFlexItem>
150
+ </VuiFlexContainer>
151
+
152
+ <VuiSpacer size="xl" />
153
+
154
+ <VuiSpans
155
+ data-testid="spansTable"
156
+ idField="id"
157
+ parentField="parentId"
158
+ rows={visibleRows}
159
+ columns={columns}
160
+ expandedIds={expandedIds}
161
+ onExpandedIdsChange={setExpandedIds}
162
+ onExpand={onExpand}
163
+ isLoading={isLoading}
164
+ content={hasError ? errorContent : undefined}
165
+ isHeaderSticky={isHeaderSticky}
166
+ fluid={fluid}
167
+ />
168
+ </>
169
+ );
170
+ };