Skip to content

Commit de9b714

Browse files
committed
fix(dashboard): improve session replay query handling and data grid selection logic
- Renamed cursor comparator variable for clarity in session replay queries. - Enhanced search query handling to include escape character for better SQL compatibility. - Introduced error handling for asynchronous queries in the Team Analytics section, providing default values for failed queries. - Refactored data grid selection logic to support flexible row selection modes and improved column width distribution. These changes enhance the robustness and usability of the dashboard components, particularly in data retrieval and user interaction.
1 parent 8ea4ccc commit de9b714

3 files changed

Lines changed: 118 additions & 68 deletions

File tree

  • apps
    • backend/src/app/api/latest/internal/session-replays
    • dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]
  • packages/dashboard-ui-components/src/components/data-grid

apps/backend/src/app/api/latest/internal/session-replays/route.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export const GET = createSmartRouteHandler({
198198
}
199199
}
200200

201-
const cursorComparator = sortDirection === "asc"
201+
const cursorWhereSql = sortDirection === "asc"
202202
? cursorPivot
203203
? Prisma.sql`AND (
204204
sr."lastEventAt" > ${cursorPivot.lastEventAt}
@@ -230,10 +230,10 @@ export const GET = createSmartRouteHandler({
230230
${durationMsMin !== null ? Prisma.sql`AND EXTRACT(EPOCH FROM (sr."lastEventAt" - sr."startedAt")) * 1000 >= ${durationMsMin}` : Prisma.empty}
231231
${durationMsMax !== null ? Prisma.sql`AND EXTRACT(EPOCH FROM (sr."lastEventAt" - sr."startedAt")) * 1000 <= ${durationMsMax}` : Prisma.empty}
232232
${searchQuery ? Prisma.sql`AND (
233-
sr."id"::text ILIKE ${`%${escapeLikePattern(searchQuery)}%`}
234-
OR pu."displayName" ILIKE ${`%${escapeLikePattern(searchQuery)}%`}
233+
sr."id"::text ILIKE ${`%${escapeLikePattern(searchQuery)}%`} ESCAPE '\'
234+
OR pu."displayName" ILIKE ${`%${escapeLikePattern(searchQuery)}%`} ESCAPE '\'
235235
)` : Prisma.empty}
236-
${cursorComparator}
236+
${cursorWhereSql}
237237
${orderBySql}
238238
LIMIT ${limit + 1}
239239
`;

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/team-analytics.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,18 @@ export function TeamAnalyticsSection({ team }: { team: ServerTeam }) {
304304
const runQuery = (query: string, params: Record<string, unknown>) =>
305305
stackAdminApp.queryAnalytics({ query, params, timeout_ms: 30_000, include_all_branches: false });
306306

307+
const emptySummary: SummaryRow = {
308+
total_events: 0,
309+
active_users_30d: 0,
310+
active_users_7d: 0,
311+
last_event_at: null,
312+
prev_total_events: 0,
313+
prev_active_users_30d: 0,
314+
prev_active_users_7d: 0,
315+
};
316+
307317
runAsynchronously(async () => {
308-
const [summaryRes, dauRes, heatmapRes, contributorsRes] = await Promise.all([
318+
const results = await Promise.allSettled([
309319
runQuery(SUMMARY_QUERY, {
310320
...baseParams,
311321
since7d: toClickhouseDateTimeParam(since7d),
@@ -323,13 +333,26 @@ export function TeamAnalyticsSection({ team }: { team: ServerTeam }) {
323333

324334
if (token.cancelled) return;
325335

336+
const queryNames = ["summary", "dau", "heatmap", "contributors"] as const;
337+
for (const [i, res] of results.entries()) {
338+
if (res.status === "rejected") {
339+
captureError(`team-analytics-query:${queryNames[i]}`, res.reason);
340+
}
341+
}
342+
if (results.every((r) => r.status === "rejected")) {
343+
setState({ status: "error" });
344+
return;
345+
}
346+
347+
const [summaryRes, dauRes, heatmapRes, contributorsRes] = results;
348+
326349
setState({
327350
status: "ready",
328351
data: {
329-
summary: parseSummary(summaryRes.result),
330-
dau: parseDau(dauRes.result),
331-
heatmap: parseHeatmap(heatmapRes.result),
332-
contributors: parseContributors(contributorsRes.result),
352+
summary: summaryRes.status === "fulfilled" ? parseSummary(summaryRes.value.result) : emptySummary,
353+
dau: dauRes.status === "fulfilled" ? parseDau(dauRes.value.result) : [],
354+
heatmap: heatmapRes.status === "fulfilled" ? parseHeatmap(heatmapRes.value.result) : [],
355+
contributors: contributorsRes.status === "fulfilled" ? parseContributors(contributorsRes.value.result) : [],
333356
},
334357
});
335358
}, {

packages/dashboard-ui-components/src/components/data-grid/data-grid.tsx

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -104,43 +104,79 @@ function resolveUpdater<T>(updater: Updater<T>, current: T): T {
104104
return typeof updater === "function" ? (updater as (old: T) => T)(current) : updater;
105105
}
106106

107+
// ─── Flex column width distribution ──────────────────────────────────
108+
109+
function distributeFlexWidths<TRow>(
110+
sizes: Record<string, number>,
111+
visibleColumns: readonly DataGridColumnDef<TRow>[],
112+
available: number,
113+
): void {
114+
const flexCols = visibleColumns.filter((c) => c.flex != null && c.flex > 0);
115+
if (flexCols.length === 0 || available <= 0) return;
116+
const totalFlex = flexCols.reduce((acc, c) => acc + (c.flex ?? 0), 0);
117+
let remaining = available;
118+
flexCols.forEach((col, i) => {
119+
const isLast = i === flexCols.length - 1;
120+
const share = isLast
121+
? remaining
122+
: Math.floor(available * ((col.flex ?? 0) / totalFlex));
123+
const max = col.maxWidth ?? Infinity;
124+
const add = Math.max(0, Math.min(share, max - sizes[col.id]));
125+
sizes[col.id] += add;
126+
remaining -= add;
127+
});
128+
}
129+
107130
// ─── Selection logic (with shift-range anchor) ───────────────────────
108131

109-
function nextSelection(
132+
type SelectionInput = {
133+
current: DataGridSelectionModel;
134+
rowId: RowId;
135+
mode: "single" | "multiple";
136+
modifiers: { shift: boolean; ctrl: boolean };
137+
allRowIds: readonly RowId[];
138+
};
139+
140+
function selectSingle(current: DataGridSelectionModel, rowId: RowId): DataGridSelectionModel {
141+
const isSelected = current.selectedIds.has(rowId);
142+
return {
143+
selectedIds: isSelected ? new Set() : new Set([rowId]),
144+
anchorId: isSelected ? null : rowId,
145+
};
146+
}
147+
148+
function selectRange(
110149
current: DataGridSelectionModel,
111150
rowId: RowId,
112-
mode: "single" | "multiple",
113-
shiftKey: boolean,
114-
ctrlKey: boolean,
115151
allRowIds: readonly RowId[],
116-
): DataGridSelectionModel {
117-
if (mode === "single") {
118-
const isSelected = current.selectedIds.has(rowId);
119-
return {
120-
selectedIds: isSelected ? new Set() : new Set([rowId]),
121-
anchorId: isSelected ? null : rowId,
122-
};
123-
}
124-
if (shiftKey && current.anchorId != null) {
125-
const anchorIdx = allRowIds.indexOf(current.anchorId);
126-
const currentIdx = allRowIds.indexOf(rowId);
127-
if (anchorIdx >= 0 && currentIdx >= 0) {
128-
const start = Math.min(anchorIdx, currentIdx);
129-
const end = Math.max(anchorIdx, currentIdx);
130-
const next = ctrlKey ? new Set(current.selectedIds) : new Set<RowId>();
131-
for (let i = start; i <= end; i++) next.add(allRowIds[i]!);
132-
return { selectedIds: next, anchorId: current.anchorId };
133-
}
134-
}
135-
if (ctrlKey) {
136-
const next = new Set(current.selectedIds);
137-
if (next.has(rowId)) {
138-
next.delete(rowId);
139-
} else {
140-
next.add(rowId);
141-
}
142-
return { selectedIds: next, anchorId: rowId };
152+
additive: boolean,
153+
): DataGridSelectionModel | null {
154+
if (current.anchorId == null) return null;
155+
const anchorIdx = allRowIds.indexOf(current.anchorId);
156+
const currentIdx = allRowIds.indexOf(rowId);
157+
if (anchorIdx < 0 || currentIdx < 0) return null;
158+
const start = Math.min(anchorIdx, currentIdx);
159+
const end = Math.max(anchorIdx, currentIdx);
160+
const next = additive ? new Set(current.selectedIds) : new Set<RowId>();
161+
for (let i = start; i <= end; i++) next.add(allRowIds[i]!);
162+
return { selectedIds: next, anchorId: current.anchorId };
163+
}
164+
165+
function selectToggle(current: DataGridSelectionModel, rowId: RowId): DataGridSelectionModel {
166+
const next = new Set(current.selectedIds);
167+
if (next.has(rowId)) next.delete(rowId);
168+
else next.add(rowId);
169+
return { selectedIds: next, anchorId: rowId };
170+
}
171+
172+
function nextSelection(input: SelectionInput): DataGridSelectionModel {
173+
const { current, rowId, mode, modifiers, allRowIds } = input;
174+
if (mode === "single") return selectSingle(current, rowId);
175+
if (modifiers.shift) {
176+
const range = selectRange(current, rowId, allRowIds, modifiers.ctrl);
177+
if (range != null) return range;
143178
}
179+
if (modifiers.ctrl) return selectToggle(current, rowId);
144180
return { selectedIds: new Set([rowId]), anchorId: rowId };
145181
}
146182

@@ -614,7 +650,7 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
614650
id: col.id,
615651
accessorFn: (row) => resolveColumnValue(col, row),
616652
header: typeof col.header === "string" ? col.header : col.id,
617-
size: col.width ?? 150,
653+
size: col.width ?? DEFAULT_COL_WIDTH,
618654
minSize: getEffectiveMinWidth(col),
619655
maxSize: getEffectiveMaxWidth(col),
620656
enableSorting: col.sortable !== false,
@@ -732,7 +768,7 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
732768
onColumnVisibilityChange: handleVisibilityChange,
733769
onColumnOrderChange: handleColumnOrderChange,
734770
onColumnPinningChange: handleColumnPinningChange,
735-
columnResizeMode: "onChange",
771+
columnResizeMode: "onEnd",
736772
enableRowSelection: selectionMode !== "none",
737773
enableMultiRowSelection: selectionMode === "multiple",
738774
enableColumnResizing: resizable,
@@ -766,31 +802,23 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
766802
}, []);
767803

768804
// ── Column width CSS variables (TanStack pattern) ────────────
769-
// Update on either columnSizing OR columnSizingInfo so resize-in-progress
770-
// also reflows widths. Cells read via var(--col-X-size).
805+
// With `columnResizeMode: "onEnd"`, live drag width comes from deltaOffset; committed sizes update on pointer-up.
771806
const columnSizingInfo = table.getState().columnSizingInfo;
772807
const columnSizes = useMemo<Record<string, number>>(() => {
773808
const sizes: Record<string, number> = {};
774809
let baseTotal = selectionMode !== "none" ? 44 : 0;
810+
const resizingId = columnSizingInfo.isResizingColumn || null;
811+
const deltaOffset = columnSizingInfo.deltaOffset ?? 0;
775812
for (const col of visibleColumns) {
776-
const s = table.getColumn(col.id)?.getSize() ?? col.width ?? DEFAULT_COL_WIDTH;
777-
sizes[col.id] = s;
778-
baseTotal += s;
779-
}
780-
const flexCols = visibleColumns.filter((c) => c.flex != null && c.flex > 0);
781-
if (flexCols.length > 0 && containerWidth > baseTotal) {
782-
const totalFlex = flexCols.reduce((acc, c) => acc + (c.flex ?? 0), 0);
783-
let remaining = containerWidth - baseTotal;
784-
flexCols.forEach((col, i) => {
785-
const share = i === flexCols.length - 1
786-
? remaining
787-
: Math.floor((containerWidth - baseTotal) * ((col.flex ?? 0) / totalFlex));
788-
const max = col.maxWidth ?? Infinity;
789-
const add = Math.max(0, Math.min(share, max - sizes[col.id]));
790-
sizes[col.id] += add;
791-
remaining -= add;
792-
});
813+
const tsCol = table.getColumn(col.id);
814+
const baseSize = tsCol?.getSize() ?? col.width ?? DEFAULT_COL_WIDTH;
815+
const liveSize = resizingId === col.id
816+
? clampColumnWidth(col, baseSize + deltaOffset)
817+
: baseSize;
818+
sizes[col.id] = liveSize;
819+
baseTotal += liveSize;
793820
}
821+
distributeFlexWidths(sizes, visibleColumns, containerWidth - baseTotal);
794822
return sizes;
795823
}, [visibleColumns, table, columnSizingInfo, state.columnWidths, containerWidth, selectionMode]);
796824

@@ -824,14 +852,13 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
824852
const handleRowClick = useCallback(
825853
(row: TRow, rowId: RowId, event: React.MouseEvent) => {
826854
if (selectionMode !== "none") {
827-
const next = nextSelection(
828-
state.selection,
855+
const next = nextSelection({
856+
current: state.selection,
829857
rowId,
830-
selectionMode,
831-
event.shiftKey,
832-
event.metaKey || event.ctrlKey,
833-
rowIds,
834-
);
858+
mode: selectionMode,
859+
modifiers: { shift: event.shiftKey, ctrl: event.metaKey || event.ctrlKey },
860+
allRowIds: rowIds,
861+
});
835862
fireSelection(next);
836863
}
837864
onRowClick?.(row, rowId, event);

0 commit comments

Comments
 (0)