@vectara/vectara-ui 16.3.1 → 16.4.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.
- package/lib/components/index.d.ts +4 -2
- package/lib/components/index.js +2 -1
- package/lib/components/spans/Spans.d.ts +23 -0
- package/lib/components/spans/Spans.js +94 -0
- package/lib/components/spans/SpansCell.d.ts +5 -0
- package/lib/components/spans/SpansCell.js +4 -0
- package/lib/components/spans/SpansHeaderCell.d.ts +6 -0
- package/lib/components/spans/SpansHeaderCell.js +5 -0
- package/lib/components/spans/SpansLoadingRow.d.ts +8 -0
- package/lib/components/spans/SpansLoadingRow.js +9 -0
- package/lib/components/spans/SpansRow.d.ts +17 -0
- package/lib/components/spans/SpansRow.js +43 -0
- package/lib/components/spans/_index.scss +110 -0
- package/lib/components/spans/buildAndFlattenSpans.d.ts +14 -0
- package/lib/components/spans/buildAndFlattenSpans.js +69 -0
- package/lib/components/spans/buildAndFlattenSpans.test.d.ts +1 -0
- package/lib/components/spans/buildAndFlattenSpans.test.js +388 -0
- package/lib/components/spans/types.d.ts +5 -0
- package/lib/components/spans/types.js +1 -0
- package/lib/styles/index.css +98 -0
- package/package.json +1 -1
- package/src/docs/pages/spans/Spans.tsx +170 -0
- package/src/docs/pages/spans/createFakeSpans.ts +118 -0
- package/src/docs/pages/spans/index.tsx +11 -0
- package/src/docs/pages.tsx +2 -1
|
@@ -79,6 +79,8 @@ import { VuiStepsVertical, StepsVertical, StepVerticalStatus } from "./stepsVert
|
|
|
79
79
|
import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
|
|
80
80
|
import { VuiSummary, VuiSummaryCitation } from "./summary";
|
|
81
81
|
import { VuiTable } from "./table/Table";
|
|
82
|
+
import { VuiSpans } from "./spans/Spans";
|
|
83
|
+
import { SpansRow } from "./spans/types";
|
|
82
84
|
import { VuiTab } from "./tabs/Tab";
|
|
83
85
|
import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
|
|
84
86
|
import { VuiTabs } from "./tabs/Tabs";
|
|
@@ -95,5 +97,5 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
|
|
|
95
97
|
import { VuiTopicButton } from "./topicButton/TopicButton";
|
|
96
98
|
import { copyToClipboard } from "../utils/copyToClipboard";
|
|
97
99
|
import { toRgb, toRgba } from "./context/Theme";
|
|
98
|
-
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
|
|
99
|
-
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
100
|
+
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, SpansRow, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
|
|
101
|
+
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
package/lib/components/index.js
CHANGED
|
@@ -72,6 +72,7 @@ import { VuiStepsVertical } from "./stepsVertical/StepsVertical";
|
|
|
72
72
|
import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
|
|
73
73
|
import { VuiSummary, VuiSummaryCitation } from "./summary";
|
|
74
74
|
import { VuiTable } from "./table/Table";
|
|
75
|
+
import { VuiSpans } from "./spans/Spans";
|
|
75
76
|
import { VuiTab } from "./tabs/Tab";
|
|
76
77
|
import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
|
|
77
78
|
import { VuiTabs } from "./tabs/Tabs";
|
|
@@ -88,4 +89,4 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
|
|
|
88
89
|
import { VuiTopicButton } from "./topicButton/TopicButton";
|
|
89
90
|
import { copyToClipboard } from "../utils/copyToClipboard";
|
|
90
91
|
import { toRgb, toRgba } from "./context/Theme";
|
|
91
|
-
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
92
|
+
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Column } from "../table/types";
|
|
3
|
+
import { SpansRow } from "./types";
|
|
4
|
+
type Props<T extends SpansRow> = {
|
|
5
|
+
idField: keyof T | ((row: T) => string);
|
|
6
|
+
parentField: keyof T | ((row: T) => string | null);
|
|
7
|
+
columns: Column<T>[];
|
|
8
|
+
rows: T[];
|
|
9
|
+
expandedIds: Set<string>;
|
|
10
|
+
onExpandedIdsChange: (ids: Set<string>) => void;
|
|
11
|
+
onExpand?: (row: T) => void | Promise<void>;
|
|
12
|
+
isLoadingChildren?: (row: T) => boolean;
|
|
13
|
+
isLoading?: boolean;
|
|
14
|
+
content?: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
|
17
|
+
indentSize?: number;
|
|
18
|
+
isHeaderSticky?: boolean;
|
|
19
|
+
fluid?: boolean;
|
|
20
|
+
"data-testid"?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const VuiSpans: <T extends SpansRow>({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize, isHeaderSticky, fluid, "data-testid": dataTestId }: Props<T>) => import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
5
|
+
import { VuiSpinner } from "../spinner/Spinner";
|
|
6
|
+
import { VuiText } from "../typography/Text";
|
|
7
|
+
import { VuiTableContent } from "../table/TableContent";
|
|
8
|
+
import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
|
|
9
|
+
import { VuiSpansRow } from "./SpansRow";
|
|
10
|
+
import { VuiSpansHeaderCell } from "./SpansHeaderCell";
|
|
11
|
+
import { VuiSpansLoadingRow } from "./SpansLoadingRow";
|
|
12
|
+
const DEFAULT_INDENT_SIZE = 16;
|
|
13
|
+
const extractId = (row, idField) => {
|
|
14
|
+
return typeof idField === "function" ? idField(row) : row[idField];
|
|
15
|
+
};
|
|
16
|
+
const extractParentId = (row, parentField) => {
|
|
17
|
+
if (typeof parentField === "function")
|
|
18
|
+
return parentField(row);
|
|
19
|
+
const value = row[parentField];
|
|
20
|
+
return value === undefined || value === null ? null : value;
|
|
21
|
+
};
|
|
22
|
+
export const VuiSpans = ({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize = DEFAULT_INDENT_SIZE, isHeaderSticky, fluid, "data-testid": dataTestId }) => {
|
|
23
|
+
const [internalLoadingIds, setInternalLoadingIds] = useState(new Set());
|
|
24
|
+
// Tracks pending fetches so we ignore stale resolutions (e.g. user collapses
|
|
25
|
+
// mid-fetch, then re-expands — the original promise should not clear loading
|
|
26
|
+
// state for the new attempt).
|
|
27
|
+
const fetchTokensRef = useRef(new Map());
|
|
28
|
+
const getId = useCallback((row) => extractId(row, idField), [idField]);
|
|
29
|
+
const getParentId = useCallback((row) => extractParentId(row, parentField), [parentField]);
|
|
30
|
+
const flatSpans = useMemo(() => buildAndFlattenSpans(rows, expandedIds, getId, getParentId), [rows, expandedIds, getId, getParentId]);
|
|
31
|
+
const columnCount = columns.length;
|
|
32
|
+
const handleToggle = useCallback((row, id, hasChildren, hasLoadedChildren) => {
|
|
33
|
+
var _a;
|
|
34
|
+
const isCurrentlyExpanded = expandedIds.has(id);
|
|
35
|
+
const nextExpandedIds = new Set(expandedIds);
|
|
36
|
+
if (isCurrentlyExpanded) {
|
|
37
|
+
nextExpandedIds.delete(id);
|
|
38
|
+
onExpandedIdsChange(nextExpandedIds);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
nextExpandedIds.add(id);
|
|
42
|
+
onExpandedIdsChange(nextExpandedIds);
|
|
43
|
+
// Only fire the consumer fetch when the row claims to have children
|
|
44
|
+
// (`hasChildren`) but none are present in the rows list yet.
|
|
45
|
+
const needsFetch = hasChildren && !hasLoadedChildren && Boolean(onExpand);
|
|
46
|
+
if (!needsFetch)
|
|
47
|
+
return;
|
|
48
|
+
const pendingFetchTokens = fetchTokensRef.current;
|
|
49
|
+
const currentFetchToken = ((_a = pendingFetchTokens.get(id)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
50
|
+
pendingFetchTokens.set(id, currentFetchToken);
|
|
51
|
+
setInternalLoadingIds((prev) => {
|
|
52
|
+
const nextLoadingIds = new Set(prev);
|
|
53
|
+
nextLoadingIds.add(id);
|
|
54
|
+
return nextLoadingIds;
|
|
55
|
+
});
|
|
56
|
+
const clearLoading = () => {
|
|
57
|
+
// Only clear if our token is still the latest — otherwise a newer
|
|
58
|
+
// fetch is in flight and owns the loading state.
|
|
59
|
+
if (pendingFetchTokens.get(id) !== currentFetchToken)
|
|
60
|
+
return;
|
|
61
|
+
setInternalLoadingIds((prev) => {
|
|
62
|
+
const nextLoadingIds = new Set(prev);
|
|
63
|
+
nextLoadingIds.delete(id);
|
|
64
|
+
return nextLoadingIds;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
Promise.resolve(onExpand(row)).finally(clearLoading);
|
|
68
|
+
}, [expandedIds, onExpandedIdsChange, onExpand]);
|
|
69
|
+
const classes = classNames("vuiSpans", className, {
|
|
70
|
+
"vuiSpans--fluid": fluid
|
|
71
|
+
});
|
|
72
|
+
let tbodyContent;
|
|
73
|
+
if (content) {
|
|
74
|
+
tbodyContent = _jsx(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: content }));
|
|
75
|
+
}
|
|
76
|
+
else if (isLoading) {
|
|
77
|
+
tbodyContent = (_jsxs(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, { children: _jsx("p", { children: "Loading" }) }) }))] })));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
tbodyContent = flatSpans.map((flat, rowIndex) => {
|
|
81
|
+
const { row, id, depth, hasChildren, hasLoadedChildren, posInSet, setSize } = flat;
|
|
82
|
+
const isExpanded = expandedIds.has(id);
|
|
83
|
+
const externalLoading = isLoadingChildren ? isLoadingChildren(row) : false;
|
|
84
|
+
const isLoadingRow = internalLoadingIds.has(id) || externalLoading;
|
|
85
|
+
const showLoadingRow = isExpanded && hasChildren && !hasLoadedChildren && isLoadingRow;
|
|
86
|
+
return (_jsxs(React.Fragment, { children: [_jsx(VuiSpansRow, { row: row, rowIndex: rowIndex, columns: columns, id: id, depth: depth, indentSize: indentSize, posInSet: posInSet, setSize: setSize, hasChildren: hasChildren, isExpanded: isExpanded, onToggle: () => handleToggle(row, id, hasChildren, hasLoadedChildren), rowDecorator: rowDecorator }), showLoadingRow && (_jsx(VuiSpansLoadingRow, { colSpan: columnCount, depth: depth + 1, indentSize: indentSize }, `${id}__loading`))] }, id));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return (_jsx("div", Object.assign({ className: "vuiSpansWrapper", "data-testid": dataTestId }, { children: _jsxs("table", Object.assign({ className: classes, role: "treegrid" }, { children: [_jsx("thead", Object.assign({ className: isHeaderSticky ? "vuiSpansStickyHeader" : undefined }, { children: _jsx("tr", Object.assign({ role: "row" }, { children: columns.map((column) => {
|
|
90
|
+
const { name, width } = column;
|
|
91
|
+
const styles = width ? { width } : undefined;
|
|
92
|
+
return (_jsx("th", Object.assign({ role: "columnheader", style: styles }, { children: _jsx(VuiSpansHeaderCell, { column: column }) }), name));
|
|
93
|
+
}) })) })), _jsx("tbody", { children: tbodyContent })] })) })));
|
|
94
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { VuiFlexContainer } from "../flex/FlexContainer";
|
|
3
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
4
|
+
import { VuiSpinner } from "../spinner/Spinner";
|
|
5
|
+
import { VuiText } from "../typography/Text";
|
|
6
|
+
import { VuiTextColor } from "../typography/TextColor";
|
|
7
|
+
export const VuiSpansLoadingRow = ({ colSpan, depth, indentSize, message = "Loading…" }) => {
|
|
8
|
+
return (_jsx("tr", Object.assign({ className: "vuiSpansLoadingRow vuiSpansRow--inert" }, { children: _jsx("td", Object.assign({ colSpan: colSpan, className: "vuiSpansLoadingRow__cell" }, { children: _jsx("div", Object.assign({ className: "vuiSpansLoadingRow__inner", style: { paddingLeft: depth * indentSize } }, { children: _jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, Object.assign({ size: "xs" }, { children: _jsx("p", { children: _jsx(VuiTextColor, Object.assign({ color: "subdued" }, { children: message })) }) })) }))] })) })) })) })));
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Column, Row } from "../table/types";
|
|
2
|
+
type Props<T> = {
|
|
3
|
+
row: T;
|
|
4
|
+
rowIndex: number;
|
|
5
|
+
columns: Column<T>[];
|
|
6
|
+
id: string;
|
|
7
|
+
depth: number;
|
|
8
|
+
indentSize: number;
|
|
9
|
+
posInSet: number;
|
|
10
|
+
setSize: number;
|
|
11
|
+
hasChildren: boolean;
|
|
12
|
+
isExpanded: boolean;
|
|
13
|
+
onToggle: () => void;
|
|
14
|
+
rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
|
15
|
+
};
|
|
16
|
+
export declare const VuiSpansRow: <T extends Row>({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }: Props<T>) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import classNames from "classnames";
|
|
14
|
+
import { BiChevronDown, BiChevronRight } from "react-icons/bi";
|
|
15
|
+
import { VuiIcon } from "../icon/Icon";
|
|
16
|
+
import { VuiIconButton } from "../button/IconButton";
|
|
17
|
+
import { VuiSpansCell } from "./SpansCell";
|
|
18
|
+
// Cap visual indentation at this depth so the first column doesn't overflow
|
|
19
|
+
// for very deep traces. `aria-level` continues to reflect the true depth.
|
|
20
|
+
const MAX_VISUAL_DEPTH = 12;
|
|
21
|
+
export const VuiSpansRow = ({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const decoratorAttrs = (_a = rowDecorator === null || rowDecorator === void 0 ? void 0 : rowDecorator(row, depth)) !== null && _a !== void 0 ? _a : {};
|
|
24
|
+
const { className: decoratorClassName } = decoratorAttrs, restDecoratorAttrs = __rest(decoratorAttrs, ["className"]);
|
|
25
|
+
const rowClassName = classNames("vuiSpansRow", decoratorClassName, {
|
|
26
|
+
"vuiSpansRow-isExpanded": hasChildren && isExpanded,
|
|
27
|
+
"vuiSpansRow--leaf": !hasChildren
|
|
28
|
+
});
|
|
29
|
+
const visualDepth = Math.min(depth, MAX_VISUAL_DEPTH);
|
|
30
|
+
const indentStyle = { paddingLeft: visualDepth * indentSize };
|
|
31
|
+
return (_jsx("tr", Object.assign({ role: "row", "aria-level": depth + 1, "aria-posinset": posInSet, "aria-setsize": setSize, "aria-expanded": hasChildren ? isExpanded : undefined, "data-row-id": id, className: rowClassName }, restDecoratorAttrs, { children: columns.map((column, columnIndex) => {
|
|
32
|
+
const { name, render, className: columnClassName, testId } = column;
|
|
33
|
+
const cellClasses = classNames("vuiSpansCellWrapper", columnClassName, {
|
|
34
|
+
"vuiSpansCellWrapper--first": columnIndex === 0,
|
|
35
|
+
"vuiSpansCellWrapper--truncate": column.truncate
|
|
36
|
+
});
|
|
37
|
+
const cellContent = render ? render(row, rowIndex) : row[name];
|
|
38
|
+
if (columnIndex === 0) {
|
|
39
|
+
return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsxs("div", Object.assign({ className: "vuiSpansCell__indent", style: indentStyle }, { children: [_jsx("div", Object.assign({ className: "vuiSpansCell__chevron" }, { children: hasChildren ? (_jsx(VuiIconButton, { icon: _jsx(VuiIcon, { children: isExpanded ? _jsx(BiChevronDown, {}) : _jsx(BiChevronRight, {}) }), size: "xs", color: "neutral", "aria-label": isExpanded ? "Collapse span" : "Expand span", onClick: onToggle, "data-testid": `spanExpandToggle-${id}` })) : (_jsx("span", { className: "vuiSpansCell__chevronPlaceholder", "aria-hidden": "true" })) })), _jsx(VuiSpansCell, { children: cellContent })] })) }), name));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsx(VuiSpansCell, { children: cellContent }) }), name));
|
|
42
|
+
}) })));
|
|
43
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
$spansChevronWidth: 24px;
|
|
2
|
+
|
|
3
|
+
.vuiSpansWrapper {
|
|
4
|
+
width: 100%;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.vuiSpans {
|
|
9
|
+
width: 100%;
|
|
10
|
+
table-layout: fixed;
|
|
11
|
+
|
|
12
|
+
thead {
|
|
13
|
+
border-bottom: 1px solid var(--vui-color-border-medium);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
tbody tr {
|
|
17
|
+
border-bottom: 1px solid var(--vui-color-border-light);
|
|
18
|
+
|
|
19
|
+
&:not(.vuiSpansRow--inert):hover {
|
|
20
|
+
background-color: rgba(var(--vui-color-light-shade-rgb), 0.25);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&:last-child {
|
|
24
|
+
border-bottom: 1px solid var(--vui-color-border-medium);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
th {
|
|
29
|
+
font-size: $fontSizeStandard;
|
|
30
|
+
font-weight: $fontWeightBold;
|
|
31
|
+
padding: $sizeXxs;
|
|
32
|
+
text-align: left;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
td {
|
|
36
|
+
font-size: $fontSizeStandard;
|
|
37
|
+
padding: $sizeXxs;
|
|
38
|
+
vertical-align: middle;
|
|
39
|
+
word-break: break-word;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.vuiSpans--fluid {
|
|
44
|
+
table-layout: auto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// First column: depth-indent + chevron + content laid out in a row.
|
|
48
|
+
.vuiSpansCell__indent {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: $sizeXxs;
|
|
52
|
+
// padding-left set inline based on depth.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.vuiSpansCell__chevron {
|
|
56
|
+
flex: 0 0 auto;
|
|
57
|
+
width: $spansChevronWidth;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.vuiSpansCell__chevronPlaceholder {
|
|
64
|
+
display: inline-block;
|
|
65
|
+
width: $spansChevronWidth;
|
|
66
|
+
height: 1px; // Reserves horizontal space; height irrelevant.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.vuiSpansCell {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
flex: 1 1 auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.vuiSpansCellWrapper--truncate {
|
|
77
|
+
min-width: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.vuiSpansHeaderCell {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
padding: $sizeXxs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sticky header — same treatment as VuiTable.
|
|
90
|
+
.vuiSpansStickyHeader {
|
|
91
|
+
position: sticky;
|
|
92
|
+
top: 0;
|
|
93
|
+
background-color: var(--vui-color-empty-shade);
|
|
94
|
+
z-index: 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Loading row injected under a parent during lazy fetch.
|
|
98
|
+
.vuiSpansLoadingRow {
|
|
99
|
+
background-color: rgba(var(--vui-color-light-shade-rgb), 0.15);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.vuiSpansLoadingRow__cell {
|
|
103
|
+
padding: $sizeXxs !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.vuiSpansLoadingRow__inner {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
// padding-left set inline based on depth.
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SpansRow } from "./types";
|
|
2
|
+
export type FlatSpan<T> = {
|
|
3
|
+
row: T;
|
|
4
|
+
id: string;
|
|
5
|
+
parentId: string | null;
|
|
6
|
+
depth: number;
|
|
7
|
+
hasChildren: boolean;
|
|
8
|
+
hasLoadedChildren: boolean;
|
|
9
|
+
posInSet: number;
|
|
10
|
+
setSize: number;
|
|
11
|
+
};
|
|
12
|
+
export type IdAccessor<T> = (row: T) => string;
|
|
13
|
+
export type ParentAccessor<T> = (row: T) => string | null;
|
|
14
|
+
export declare const buildAndFlattenSpans: <T extends SpansRow>(rows: T[], expandedIds: Set<string>, getId: IdAccessor<T>, getParentId: ParentAccessor<T>) => FlatSpan<T>[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Flattens a list of rows (linked by `parentId`) into a render-ordered, depth-tagged array,
|
|
2
|
+
// showing only visible nodes based on `expandedIds`. The render layer maps this 1:1 to <tr>s.
|
|
3
|
+
// Orphans (rows with missing parents) are dropped. Cycles are avoided using an ancestor set.
|
|
4
|
+
export const buildAndFlattenSpans = (rows, expandedIds, getId, getParentId) => {
|
|
5
|
+
// First pass: collect every known id so we can detect orphans below.
|
|
6
|
+
const allIds = new Set();
|
|
7
|
+
for (const row of rows) {
|
|
8
|
+
allIds.add(getId(row));
|
|
9
|
+
}
|
|
10
|
+
// Group rows by their parent id, with top-level rows under a sentinel key.
|
|
11
|
+
// The sentinel can't collide with a real id because it contains spaces.
|
|
12
|
+
const ROOT_KEY = " __VuiSpansRoot__ ";
|
|
13
|
+
const childrenByParent = new Map();
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
const parentId = getParentId(row);
|
|
16
|
+
// Drop orphans: parent id set but not present in the rows list.
|
|
17
|
+
if (parentId !== null && !allIds.has(parentId))
|
|
18
|
+
continue;
|
|
19
|
+
const key = parentId === null ? ROOT_KEY : parentId;
|
|
20
|
+
const list = childrenByParent.get(key);
|
|
21
|
+
if (list) {
|
|
22
|
+
list.push(row);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
childrenByParent.set(key, [row]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const result = [];
|
|
29
|
+
// Recursive depth-first walk. `ancestors` carries the chain of ids from the
|
|
30
|
+
// root down to (but not including) the current row, used to detect cycles.
|
|
31
|
+
const walk = (parentKey, parentId, depth, ancestors) => {
|
|
32
|
+
const siblings = childrenByParent.get(parentKey);
|
|
33
|
+
if (!siblings)
|
|
34
|
+
return;
|
|
35
|
+
siblings.forEach((row, index) => {
|
|
36
|
+
const id = getId(row);
|
|
37
|
+
// Break cycle: this id appears earlier in its own ancestor chain.
|
|
38
|
+
if (ancestors.has(id))
|
|
39
|
+
return;
|
|
40
|
+
const ownChildren = childrenByParent.get(id);
|
|
41
|
+
const ownChildCount = ownChildren ? ownChildren.length : 0;
|
|
42
|
+
const hasLoadedChildren = ownChildCount > 0;
|
|
43
|
+
// The chevron should appear if either we already have children to show
|
|
44
|
+
// or the row is hinting that lazily-loadable children exist.
|
|
45
|
+
const hasChildren = row.hasChildren || hasLoadedChildren;
|
|
46
|
+
result.push({
|
|
47
|
+
row,
|
|
48
|
+
id,
|
|
49
|
+
parentId,
|
|
50
|
+
depth,
|
|
51
|
+
hasChildren,
|
|
52
|
+
hasLoadedChildren,
|
|
53
|
+
posInSet: index + 1,
|
|
54
|
+
setSize: siblings.length
|
|
55
|
+
});
|
|
56
|
+
// Only descend if the user has expanded this row AND there's something
|
|
57
|
+
// loaded to descend into. The lazy-only case (`hasChildren` but
|
|
58
|
+
// `!hasLoadedChildren`) is handled by the loading row in the render
|
|
59
|
+
// layer, not here.
|
|
60
|
+
if (hasChildren && expandedIds.has(id) && ownChildCount > 0) {
|
|
61
|
+
const nextAncestors = new Set(ancestors);
|
|
62
|
+
nextAncestors.add(id);
|
|
63
|
+
walk(id, id, depth + 1, nextAncestors);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
walk(ROOT_KEY, null, 0, new Set());
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
package/lib/styles/index.css
CHANGED
|
@@ -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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type FakeSpan = {
|
|
2
|
+
id: string;
|
|
3
|
+
parentId: string | null;
|
|
4
|
+
name: string;
|
|
5
|
+
kind: "workflow" | "tool" | "llm" | "search" | "embedding";
|
|
6
|
+
status: "ok" | "error" | "running";
|
|
7
|
+
startAt: string;
|
|
8
|
+
durationMs: number;
|
|
9
|
+
hasChildren?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ROOT_SPANS: FakeSpan[] = [
|
|
13
|
+
{
|
|
14
|
+
id: "wf-1",
|
|
15
|
+
parentId: null,
|
|
16
|
+
name: "Run search workflow",
|
|
17
|
+
kind: "workflow",
|
|
18
|
+
status: "ok",
|
|
19
|
+
startAt: "10:42:17.103",
|
|
20
|
+
durationMs: 3420
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "wf-1-c1",
|
|
24
|
+
parentId: "wf-1",
|
|
25
|
+
name: "Generate query embeddings",
|
|
26
|
+
kind: "embedding",
|
|
27
|
+
status: "ok",
|
|
28
|
+
startAt: "10:42:17.110",
|
|
29
|
+
durationMs: 142
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "wf-1-c2",
|
|
33
|
+
parentId: "wf-1",
|
|
34
|
+
name: "Vector search (corpus: support_docs)",
|
|
35
|
+
kind: "search",
|
|
36
|
+
status: "ok",
|
|
37
|
+
startAt: "10:42:17.260",
|
|
38
|
+
durationMs: 980,
|
|
39
|
+
// Lazy: chevron shown but no child rows present in initial fetch.
|
|
40
|
+
hasChildren: true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "wf-1-c3",
|
|
44
|
+
parentId: "wf-1",
|
|
45
|
+
name: "Re-rank top-25 candidates",
|
|
46
|
+
kind: "tool",
|
|
47
|
+
status: "ok",
|
|
48
|
+
startAt: "10:42:18.250",
|
|
49
|
+
durationMs: 412
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "wf-1-c4",
|
|
53
|
+
parentId: "wf-1",
|
|
54
|
+
name: "Generate summary",
|
|
55
|
+
kind: "llm",
|
|
56
|
+
status: "running",
|
|
57
|
+
startAt: "10:42:18.670",
|
|
58
|
+
durationMs: 1850
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "wf-1-c4-c1",
|
|
62
|
+
parentId: "wf-1-c4",
|
|
63
|
+
name: "Build prompt context",
|
|
64
|
+
kind: "tool",
|
|
65
|
+
status: "ok",
|
|
66
|
+
startAt: "10:42:18.671",
|
|
67
|
+
durationMs: 12
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "wf-1-c4-c2",
|
|
71
|
+
parentId: "wf-1-c4",
|
|
72
|
+
name: "claude-haiku-4-5 streaming call",
|
|
73
|
+
kind: "llm",
|
|
74
|
+
status: "running",
|
|
75
|
+
startAt: "10:42:18.700",
|
|
76
|
+
durationMs: 1820
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "wf-2",
|
|
80
|
+
parentId: null,
|
|
81
|
+
name: "Persist trace",
|
|
82
|
+
kind: "tool",
|
|
83
|
+
status: "error",
|
|
84
|
+
startAt: "10:42:20.523",
|
|
85
|
+
durationMs: 38
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// Children of "wf-1-c2"
|
|
90
|
+
export const LAZY_CHILDREN: FakeSpan[] = [
|
|
91
|
+
{
|
|
92
|
+
id: "wf-1-c2-c1",
|
|
93
|
+
parentId: "wf-1-c2",
|
|
94
|
+
name: "Search Vectara API",
|
|
95
|
+
kind: "search",
|
|
96
|
+
status: "ok",
|
|
97
|
+
startAt: "10:42:17.262",
|
|
98
|
+
durationMs: 940
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "wf-1-c2-c2",
|
|
102
|
+
parentId: "wf-1-c2",
|
|
103
|
+
name: "Parse filter expression",
|
|
104
|
+
kind: "tool",
|
|
105
|
+
status: "ok",
|
|
106
|
+
startAt: "10:42:17.265",
|
|
107
|
+
durationMs: 4
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "wf-1-c2-c3",
|
|
111
|
+
parentId: "wf-1-c2",
|
|
112
|
+
name: "Apply MMR diversification",
|
|
113
|
+
kind: "tool",
|
|
114
|
+
status: "ok",
|
|
115
|
+
startAt: "10:42:18.205",
|
|
116
|
+
durationMs: 32
|
|
117
|
+
}
|
|
118
|
+
];
|
package/src/docs/pages.tsx
CHANGED
|
@@ -57,6 +57,7 @@ import { summary } from "./pages/summary";
|
|
|
57
57
|
import { skeleton } from "./pages/skeleton";
|
|
58
58
|
import { superCheckboxGroup } from "./pages/superCheckboxGroup";
|
|
59
59
|
import { superRadioGroup } from "./pages/superRadioGroup";
|
|
60
|
+
import { spans } from "./pages/spans";
|
|
60
61
|
import { table } from "./pages/table";
|
|
61
62
|
import { tabs } from "./pages/tabs";
|
|
62
63
|
import { text } from "./pages/text";
|
|
@@ -87,7 +88,7 @@ export const categories: Category[] = [
|
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
90
|
name: "Info",
|
|
90
|
-
pages: [table, infoTable, infoList, statList, list, pagination]
|
|
91
|
+
pages: [table, spans, infoTable, infoList, statList, list, pagination]
|
|
91
92
|
},
|
|
92
93
|
{
|
|
93
94
|
name: "Layout",
|