update dashboard controller to be more memoized

This commit is contained in:
Nate Kelley 2025-04-18 23:01:40 -06:00
parent 0973733b46
commit bb74d9c30b
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
2 changed files with 298 additions and 286 deletions

View File

@ -45,310 +45,314 @@ export const BusterResizeableGrid: React.FC<{
onEndDrag?: (d: { id: string }) => void;
overlayComponent?: React.ReactNode;
readOnly?: boolean;
}> = ({
readOnly = true,
className = '',
overlayComponent,
rows: serverRows,
onRowLayoutChange,
onStartDrag,
onEndDrag
}) => {
const [rows, setRows] = useState<BusterResizeableGridRow[]>(serverRows);
const styleRef = useRef<HTMLStyleElement>(undefined);
}> = React.memo(
({
readOnly = true,
className = '',
overlayComponent,
rows: serverRows,
onRowLayoutChange,
onStartDrag,
onEndDrag
}) => {
const [rows, setRows] = useState<BusterResizeableGridRow[]>(serverRows);
const styleRef = useRef<HTMLStyleElement>(undefined);
const onRowLayoutChangePreflight = useMemoizedFn((newLayout: BusterResizeableGridRow[]) => {
const filteredRows = newRowPreflight(newLayout);
const onRowLayoutChangePreflight = useMemoizedFn((newLayout: BusterResizeableGridRow[]) => {
const filteredRows = newRowPreflight(newLayout);
if (checkRowEquality(filteredRows, rows)) {
return;
}
onRowLayoutChange(filteredRows);
setRows(filteredRows);
});
//Dnd Kit stuff
const [activeId, setActiveId] = useState<string | null>(null);
const lastOverId = useRef<string | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const isAnimating = useRef(false);
const sensors = useSensors(useSensor(PointerSensor, pointerSensors));
const collisionDetectionStrategy: CollisionDetection = useMemoizedFn((args) => {
if (activeId && rows.some((row) => row.id === activeId)) {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter((container) =>
rows.map((v) => v.id).includes(container.id as string)
)
});
}
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0
? // If there are droppables intersecting with the pointer, return those
pointerIntersections
: []; //rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
if (overId != null) {
if (rows.some((row) => row.id === overId)) {
const containerItems = rows.find((row) => row.id === overId)?.items || [];
// If a container is matched and it contains items (columns 'A', 'B', 'C')
if (containerItems.length > 0) {
// Return the closest droppable within that container
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) =>
container.id !== overId &&
containerItems.map((v) => v.id).includes(container.id as string)
)
})[0]?.id;
}
}
lastOverId.current = overId as string;
return [{ id: overId }];
}
// When a draggable item moves to a new container, the layout may shift
// and the `overId` may become `null`. We manually set the cached `lastOverId`
// to the id of the draggable item that was moved to the new container, otherwise
// the previous `overId` will be returned which can cause items to incorrectly shift positions
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeId;
}
// If no droppable is matched, return the last match
return lastOverId.current ? [{ id: lastOverId.current }] : [];
});
const findContainer = useMemoizedFn((id: string) => {
if (rows.some((row) => row.id === id)) {
return id;
}
return rows.find((row) => row.items.some((item) => item.id === id))?.id;
});
const onDragCancel = useMemoizedFn(() => {
if (serverRows) {
// Reset items to their original state in case items have been
// Dragged across containers
onRowLayoutChangePreflight(serverRows);
}
setActiveId(null);
});
const onDragStart = useMemoizedFn(({ active }: DragStartEvent) => {
const style = document.createElement('style');
style.innerHTML = `* { cursor: grabbing; }`;
document.head.appendChild(style);
styleRef.current = style;
setActiveId(active.id as string);
onStartDrag?.({ id: active.id as string });
isAnimating.current = true;
});
const onDragEnd = useMemoizedFn(
({ over, active, delta, activatorEvent, collisions }: DragEndEvent) => {
document.body.style.cursor = '';
if (styleRef.current) {
document.head.removeChild(styleRef.current);
}
const activeContainer = findContainer(active.id as string);
onEndDrag?.({ id: active.id as string });
if (!activeContainer) {
setActiveId(null);
if (checkRowEquality(filteredRows, rows)) {
return;
}
onRowLayoutChange(filteredRows);
setRows(filteredRows);
});
const overId = over?.id as string;
//Dnd Kit stuff
const [activeId, setActiveId] = useState<string | null>(null);
const lastOverId = useRef<string | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const isAnimating = useRef(false);
if (overId == null) {
setActiveId(null);
return;
}
const sensors = useSensors(useSensor(PointerSensor, pointerSensors));
//COMPLETELY NEW ROW!
if (overId.includes(NEW_ROW_ID)) {
const newRowId = uuidv4();
const newRowDroppedId = over?.data.current?.id;
const filteredRows = rows.map((row) => {
if (row.id === activeContainer) {
return {
...row,
items: row.items.filter((item) => item.id !== active.id)
};
}
return row;
const collisionDetectionStrategy: CollisionDetection = useMemoizedFn((args) => {
if (activeId && rows.some((row) => row.id === activeId)) {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter((container) =>
rows.map((v) => v.id).includes(container.id as string)
)
});
const newRow =
rows
.find((row) => row.id === activeContainer)
?.items.filter((item) => item.id === active.id) || [];
const newRowConfig = {
id: newRowId,
items: newRow,
columnSizes: [12],
rowHeight: MIN_ROW_HEIGHT
};
if (newRowDroppedId === TOP_SASH_ID) {
return onRowLayoutChangePreflight([newRowConfig, ...filteredRows]);
}
if (newRowDroppedId) {
const numericId = parseInt(newRowDroppedId) + 1;
const newRows = filteredRows.reduce<BusterResizeableGridRow[]>((acc, row, index) => {
if (index === numericId) {
acc.push(newRowConfig);
}
acc.push(row);
return acc;
}, [] as BusterResizeableGridRow[]);
return onRowLayoutChangePreflight(newRows);
}
return onRowLayoutChangePreflight([...filteredRows, newRowConfig]);
}
const overContainer = findContainer(overId as string);
const numberOfItemsInOverExeeds = over?.data.current?.sortable?.items.length >= 4;
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0
? // If there are droppables intersecting with the pointer, return those
pointerIntersections
: []; //rectIntersection(args);
if (overContainer !== activeContainer && numberOfItemsInOverExeeds) {
setActiveId(null);
return;
let overId = getFirstCollision(intersections, 'id');
if (overId != null) {
if (rows.some((row) => row.id === overId)) {
const containerItems = rows.find((row) => row.id === overId)?.items || [];
// If a container is matched and it contains items (columns 'A', 'B', 'C')
if (containerItems.length > 0) {
// Return the closest droppable within that container
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container) =>
container.id !== overId &&
containerItems.map((v) => v.id).includes(container.id as string)
)
})[0]?.id;
}
}
lastOverId.current = overId as string;
return [{ id: overId }];
}
if (activeContainer !== overContainer && !overId.includes(NEW_ROW_ID)) {
const activeItems = rows.find((row) => row.id === activeContainer)?.items || [];
const overItems = rows.find((row) => row.id === overContainer)?.items || [];
const overIndex = overItems.findIndex((item) => item.id === overId);
const activeIndex = activeItems.findIndex((item) => item.id === active.id);
// When a draggable item moves to a new container, the layout may shift
// and the `overId` may become `null`. We manually set the cached `lastOverId`
// to the id of the draggable item that was moved to the new container, otherwise
// the previous `overId` will be returned which can cause items to incorrectly shift positions
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeId;
}
const isOverLastItem =
over?.id === overId && over?.data.current?.sortable?.index === overItems.length - 1;
let modifier = 0;
if (isOverLastItem) {
const widthOfItem = over?.rect.width;
const leftSideOfItem = over?.rect.left;
const initialMouseX = (activatorEvent as MouseEvent)?.clientX || 0;
const movedDistanceX = initialMouseX + delta.x;
const mouseLeft = movedDistanceX;
const isOverLeftHalf = mouseLeft < leftSideOfItem + widthOfItem / 2;
modifier = isOverLeftHalf ? 0 : 1;
// If no droppable is matched, return the last match
return lastOverId.current ? [{ id: lastOverId.current }] : [];
});
const findContainer = useMemoizedFn((id: string) => {
if (rows.some((row) => row.id === id)) {
return id;
}
return rows.find((row) => row.items.some((item) => item.id === id))?.id;
});
const onDragCancel = useMemoizedFn(() => {
if (serverRows) {
// Reset items to their original state in case items have been
// Dragged across containers
onRowLayoutChangePreflight(serverRows);
}
setActiveId(null);
});
const onDragStart = useMemoizedFn(({ active }: DragStartEvent) => {
const style = document.createElement('style');
style.innerHTML = `* { cursor: grabbing; }`;
document.head.appendChild(style);
styleRef.current = style;
setActiveId(active.id as string);
onStartDrag?.({ id: active.id as string });
isAnimating.current = true;
});
const onDragEnd = useMemoizedFn(
({ over, active, delta, activatorEvent, collisions }: DragEndEvent) => {
document.body.style.cursor = '';
if (styleRef.current) {
document.head.removeChild(styleRef.current);
}
const newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1;
const activeContainer = findContainer(active.id as string);
onEndDrag?.({ id: active.id as string });
recentlyMovedToNewContainer.current = true;
if (!activeContainer) {
setActiveId(null);
return;
}
return onRowLayoutChangePreflight([
...rows.map((row) => {
const overId = over?.id as string;
if (overId == null) {
setActiveId(null);
return;
}
//COMPLETELY NEW ROW!
if (overId.includes(NEW_ROW_ID)) {
const newRowId = uuidv4();
const newRowDroppedId = over?.data.current?.id;
const filteredRows = rows.map((row) => {
if (row.id === activeContainer) {
return {
...row,
items: row.items.filter((item) => item.id !== active.id)
};
}
if (row.id === overContainer) {
return {
...row,
items: [
...row.items.slice(0, newIndex),
activeItems[activeIndex],
...row.items.slice(newIndex, row.items.length)
]
};
}
return row;
})
]);
}
if (overContainer) {
const activeIndex = rows
.find((r) => r.id === activeContainer)
?.items.findIndex((row) => row.id === active.id);
const overIndex = rows
.find((r) => r.id === overContainer)
?.items.findIndex((row) => row.id === overId);
if (activeIndex !== overIndex && activeIndex !== undefined && overIndex !== undefined) {
const newRows = rows.map((row) => {
if (row.id === overContainer) {
return {
...row,
items: arrayMove(row.items, activeIndex, overIndex),
columnSizes: arrayMove(row.columnSizes!, activeIndex, overIndex)
};
}
return row;
});
const newRow =
rows
.find((row) => row.id === activeContainer)
?.items.filter((item) => item.id === active.id) || [];
onRowLayoutChangePreflight(newRows);
} else {
onRowLayoutChangePreflight(rows);
const newRowConfig = {
id: newRowId,
items: newRow,
columnSizes: [12],
rowHeight: MIN_ROW_HEIGHT
};
if (newRowDroppedId === TOP_SASH_ID) {
return onRowLayoutChangePreflight([newRowConfig, ...filteredRows]);
}
if (newRowDroppedId) {
const numericId = parseInt(newRowDroppedId) + 1;
const newRows = filteredRows.reduce<BusterResizeableGridRow[]>((acc, row, index) => {
if (index === numericId) {
acc.push(newRowConfig);
}
acc.push(row);
return acc;
}, [] as BusterResizeableGridRow[]);
return onRowLayoutChangePreflight(newRows);
}
return onRowLayoutChangePreflight([...filteredRows, newRowConfig]);
}
const overContainer = findContainer(overId as string);
const numberOfItemsInOverExeeds = over?.data.current?.sortable?.items.length >= 4;
if (overContainer !== activeContainer && numberOfItemsInOverExeeds) {
setActiveId(null);
return;
}
if (activeContainer !== overContainer && !overId.includes(NEW_ROW_ID)) {
const activeItems = rows.find((row) => row.id === activeContainer)?.items || [];
const overItems = rows.find((row) => row.id === overContainer)?.items || [];
const overIndex = overItems.findIndex((item) => item.id === overId);
const activeIndex = activeItems.findIndex((item) => item.id === active.id);
const isOverLastItem =
over?.id === overId && over?.data.current?.sortable?.index === overItems.length - 1;
let modifier = 0;
if (isOverLastItem) {
const widthOfItem = over?.rect.width;
const leftSideOfItem = over?.rect.left;
const initialMouseX = (activatorEvent as MouseEvent)?.clientX || 0;
const movedDistanceX = initialMouseX + delta.x;
const mouseLeft = movedDistanceX;
const isOverLeftHalf = mouseLeft < leftSideOfItem + widthOfItem / 2;
modifier = isOverLeftHalf ? 0 : 1;
}
const newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1;
recentlyMovedToNewContainer.current = true;
return onRowLayoutChangePreflight([
...rows.map((row) => {
if (row.id === activeContainer) {
return {
...row,
items: row.items.filter((item) => item.id !== active.id)
};
}
if (row.id === overContainer) {
return {
...row,
items: [
...row.items.slice(0, newIndex),
activeItems[activeIndex],
...row.items.slice(newIndex, row.items.length)
]
};
}
return row;
})
]);
}
if (overContainer) {
const activeIndex = rows
.find((r) => r.id === activeContainer)
?.items.findIndex((row) => row.id === active.id);
const overIndex = rows
.find((r) => r.id === overContainer)
?.items.findIndex((row) => row.id === overId);
if (activeIndex !== overIndex && activeIndex !== undefined && overIndex !== undefined) {
const newRows = rows.map((row) => {
if (row.id === overContainer) {
return {
...row,
items: arrayMove(row.items, activeIndex, overIndex),
columnSizes: arrayMove(row.columnSizes!, activeIndex, overIndex)
};
}
return row;
});
onRowLayoutChangePreflight(newRows);
} else {
onRowLayoutChangePreflight(rows);
}
}
setActiveId(null);
}
);
setActiveId(null);
}
);
useEffect(() => {
requestAnimationFrame(() => {
setTimeout(() => {
recentlyMovedToNewContainer.current = false;
isAnimating.current = false;
}, 50);
});
}, [rows]);
useEffect(() => {
requestAnimationFrame(() => {
setTimeout(() => {
recentlyMovedToNewContainer.current = false;
isAnimating.current = false;
}, 50);
});
}, [rows]);
useEffect(() => {
if (!checkRowEquality(serverRows, rows) && !isAnimating.current) {
setRows(serverRows);
}
}, [serverRows]);
useEffect(() => {
if (!checkRowEquality(serverRows, rows) && !isAnimating.current) {
setRows(serverRows);
}
}, [serverRows]);
return (
<DndContext
measuring={measuringConfig}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragCancel={onDragCancel}
collisionDetection={collisionDetectionStrategy}
sensors={sensors}>
<div className={cn('buster-resizeable-grid h-full w-full', className)}>
<BusterResizeRows
rows={rows}
className={className}
readOnly={readOnly}
onRowLayoutChange={onRowLayoutChangePreflight}
/>
</div>
return (
<DndContext
measuring={measuringConfig}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragCancel={onDragCancel}
collisionDetection={collisionDetectionStrategy}
sensors={sensors}>
<div className={cn('buster-resizeable-grid h-full w-full', className)}>
<BusterResizeRows
rows={rows}
className={className}
readOnly={readOnly}
onRowLayoutChange={onRowLayoutChangePreflight}
/>
</div>
{!readOnly && (
<BusterSortableOverlay
activeId={activeId}
overlayComponent={overlayComponent}
rows={rows}
/>
)}
</DndContext>
);
}
);
{!readOnly && (
<BusterSortableOverlay
activeId={activeId}
overlayComponent={overlayComponent}
rows={rows}
/>
)}
</DndContext>
);
};
BusterResizeableGrid.displayName = 'BusterResizeableGrid';
const removeEmptyContainers = (items: BusterResizeableGridRow[]): BusterResizeableGridRow[] => {
return items.filter((item) => item.items.length > 0 && item.items[0]?.id);

View File

@ -55,6 +55,23 @@ export const DashboardContentController: React.FC<{
return remapMetrics ? normalizeNewMetricsIntoGrid(metrics, configRows) : configRows;
}, [remapMetrics, metrics, configRows]);
const memoizedOverlayComponent = useMemo(() => {
return (
dashboard &&
draggingId && (
<DashboardMetricItem
metricId={draggingId}
readOnly={true}
dashboardId={dashboard?.id}
isDragOverlay
numberOfMetrics={numberOfMetrics}
chatId={undefined}
versionNumber={metrics[draggingId]?.version_number}
/>
)
);
}, [draggingId, dashboard?.id, numberOfMetrics, metrics]);
const dashboardRows = useMemo(() => {
console.log('dashboardRows! rerender', rows);
return rows
@ -90,9 +107,12 @@ export const DashboardContentController: React.FC<{
{ wait: 650, leading: true }
);
const onRowLayoutChange = useMemoizedFn((rows: BusterResizeableGridRow[]) => {
const onRowLayoutChange = useMemoizedFn((layoutRows: BusterResizeableGridRow[]) => {
if (dashboard) {
onUpdateDashboardConfig({ rows: removeChildrenFromItems(rows), dashboardId: dashboard.id });
onUpdateDashboardConfig({
rows: removeChildrenFromItems(layoutRows),
dashboardId: dashboard.id
});
}
});
@ -132,19 +152,7 @@ export const DashboardContentController: React.FC<{
onRowLayoutChange={onRowLayoutChange}
onStartDrag={onStartDrag}
onEndDrag={onDragEnd}
overlayComponent={
draggingId && (
<DashboardMetricItem
metricId={draggingId}
readOnly={true}
dashboardId={dashboard.id}
isDragOverlay
numberOfMetrics={numberOfMetrics}
chatId={undefined}
versionNumber={metrics[draggingId]?.version_number}
/>
)
}
overlayComponent={memoizedOverlayComponent}
/>
</DashboardContentControllerProvider>
) : !readOnly ? (