mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
🖼️ style: Improve Marketplace & Sharing Dialog UI
feat: Enhance CategoryTabs and Marketplace components for better responsiveness and navigation feat: Refactor AgentCard and AgentGrid components for improved layout and accessibility feat: Implement animated category transitions in AgentMarketplace and update NewChat component layout feat: Refactor UI components for improved styling and accessibility in sharing dialogs refactor: remove GenericManagePermissionsDialog and GrantAccessDialog components - Deleted GenericManagePermissionsDialog and GrantAccessDialog components to streamline sharing functionality. - Updated ManagePermissionsDialog to utilize AccessRolesPicker directly. - Introduced UnifiedPeopleSearch for improved people selection experience. - Enhanced PublicSharingToggle with InfoHoverCard for better user guidance. - Adjusted AgentPanel to change error status to warning for duplicate agent versions. - Updated translations to include new keys for search and access management. feat: Add responsive design for SelectedPrincipalsList and improve layout in GenericGrantAccessDialog feat: Enhance styling in SelectedPrincipalsList and SearchPicker components for improved UI consistency feat: Improve PublicSharingToggle component with enhanced styling and accessibility features feat: Introduce InfoHoverCard component and refactor enums for better organization feat: Implement infinite scroll for agent grids and enhance performance - Added `useInfiniteScroll` hook to manage infinite scrolling behavior in agent grids. - Integrated infinite scroll functionality into `AgentGrid` and `VirtualizedAgentGrid` components. - Updated `AgentMarketplace` to pass the scroll container to the agent grid components. - Refactored loading indicators to show a spinner instead of a "Load More" button. - Created `VirtualizedAgentGrid` component for optimized rendering of agent cards using virtualization. - Added performance tests for `VirtualizedAgentGrid` to ensure efficient handling of large datasets. - Updated translations to include new messages for end-of-results scenarios. chore: Remove unused permission-related UI localization keys ci: Update Agent model tests to handle duplicate support_contact updates - Modified tests to ensure that updating an agent with the same support_contact does not create a new version and returns successfully. - Enhanced verification for partial changes in support_contact, confirming no new version is created when content remains the same. chore: Address ESLint, clean up unused imports and improve prop definitions in various components ci: fix tests ci: update tests chore: remove unused search localization keys
This commit is contained in:
parent
9585db14ba
commit
d82a63642d
51 changed files with 2074 additions and 1311 deletions
|
|
@ -0,0 +1,275 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { jest } from '@jest/globals';
|
||||
import VirtualizedAgentGrid from '../VirtualizedAgentGrid';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
|
||||
// Mock react-virtualized for performance testing
|
||||
const mockRowRenderer = jest.fn();
|
||||
|
||||
jest.mock('react-virtualized', () => {
|
||||
const mockRowRendererRef = { current: jest.fn() };
|
||||
|
||||
return {
|
||||
AutoSizer: ({
|
||||
children,
|
||||
disableHeight,
|
||||
}: {
|
||||
children: (props: { width: number; height?: number }) => React.ReactNode;
|
||||
disableHeight?: boolean;
|
||||
}) => {
|
||||
if (disableHeight) {
|
||||
return children({ width: 1200 });
|
||||
}
|
||||
return children({ width: 1200, height: 800 });
|
||||
},
|
||||
List: ({
|
||||
rowRenderer,
|
||||
rowCount,
|
||||
autoHeight,
|
||||
height,
|
||||
width,
|
||||
rowHeight,
|
||||
overscanRowCount,
|
||||
scrollTop,
|
||||
isScrolling,
|
||||
onScroll,
|
||||
style,
|
||||
'aria-rowcount': ariaRowCount,
|
||||
'data-testid': dataTestId,
|
||||
'data-total-rows': dataTotalRows,
|
||||
}: {
|
||||
rowRenderer: any;
|
||||
rowCount: number;
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
// Store the row renderer for testing
|
||||
if (typeof rowRenderer === 'function') {
|
||||
mockRowRendererRef.current = rowRenderer;
|
||||
mockRowRenderer.mockImplementation(rowRenderer);
|
||||
}
|
||||
// Only render visible rows to simulate virtualization
|
||||
const visibleRows = Math.min(10, rowCount); // Simulate 10 visible rows
|
||||
return (
|
||||
<div
|
||||
data-testid={dataTestId || 'virtual-list'}
|
||||
data-total-rows={dataTotalRows || rowCount}
|
||||
aria-rowcount={ariaRowCount}
|
||||
style={style}
|
||||
>
|
||||
{Array.from({ length: visibleRows }, (_, index) =>
|
||||
rowRenderer({
|
||||
index,
|
||||
key: `row-${index}`,
|
||||
style: { height: 184 },
|
||||
parent: { props: { width: width || 1200 } },
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
WindowScroller: ({
|
||||
children,
|
||||
scrollElement,
|
||||
}: {
|
||||
children: (props: any) => React.ReactNode;
|
||||
scrollElement?: HTMLElement | null;
|
||||
}) => {
|
||||
return children({
|
||||
height: 800,
|
||||
isScrolling: false,
|
||||
registerChild: (ref: any) => {},
|
||||
onChildScroll: () => {},
|
||||
scrollTop: 0,
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Generate large dataset for performance testing
|
||||
const generateLargeDataset = (count: number) => {
|
||||
const agents: Partial<t.Agent>[] = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
agents.push({
|
||||
id: `agent-${i}`,
|
||||
name: `Performance Test Agent ${i}`,
|
||||
description: `This is agent ${i} for performance testing virtual scrolling with large datasets`,
|
||||
category: i % 2 === 0 ? 'productivity' : 'development',
|
||||
});
|
||||
}
|
||||
return agents;
|
||||
};
|
||||
|
||||
// Mock the data provider with large dataset
|
||||
const createMockInfiniteQuery = (agentCount: number) => ({
|
||||
data: {
|
||||
pages: [{ data: generateLargeDataset(agentCount) }],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
fetchNextPage: jest.fn(),
|
||||
hasNextPage: false,
|
||||
refetch: jest.fn(),
|
||||
isFetchingNextPage: false,
|
||||
});
|
||||
|
||||
// Mock must be hoisted before imports
|
||||
jest.mock('~/data-provider/Agents', () => ({
|
||||
useMarketplaceAgentsInfiniteQuery: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentCategories: () => ({
|
||||
categories: [
|
||||
{ value: 'productivity', label: 'Productivity' },
|
||||
{ value: 'development', label: 'Development' },
|
||||
],
|
||||
}),
|
||||
useLocalize: () => (key: string, params?: any) => {
|
||||
if (key === 'com_agents_grid_announcement') {
|
||||
return `Found ${params?.count || 0} agents in ${params?.category || 'category'}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../SmartLoader', () => ({
|
||||
useHasData: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('../AgentCard', () => {
|
||||
return function MockAgentCard({ agent }: { agent: any }) {
|
||||
return (
|
||||
<div data-testid={`agent-card-${agent.id}`} style={{ height: '160px' }}>
|
||||
<h3>{agent.name}</h3>
|
||||
<p>{agent.description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
describe('Virtual Scrolling Performance', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
mockRowRenderer.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = (agentCount: number) => {
|
||||
const mockQuery = createMockInfiniteQuery(agentCount);
|
||||
const useMarketplaceAgentsInfiniteQuery =
|
||||
jest.requireMock('~/data-provider/Agents').useMarketplaceAgentsInfiniteQuery;
|
||||
useMarketplaceAgentsInfiniteQuery.mockReturnValue(mockQuery);
|
||||
|
||||
// Clear previous mock calls
|
||||
mockRowRenderer.mockClear();
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<VirtualizedAgentGrid category="all" searchQuery="" onSelectAgent={jest.fn()} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
it('efficiently handles 1000 agents without rendering all DOM nodes', () => {
|
||||
const startTime = performance.now();
|
||||
renderComponent(1000);
|
||||
const endTime = performance.now();
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '500'); // 1000 agents / 2 per row
|
||||
|
||||
// Should only render visible cards, not all 1000
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThan(50); // Much less than 1000
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
|
||||
// Performance check: rendering should be fast
|
||||
const renderTime = endTime - startTime;
|
||||
expect(renderTime).toBeLessThan(600); // Should render in less than 600ms
|
||||
|
||||
console.log(`Rendered 1000 agents in ${renderTime.toFixed(2)}ms`);
|
||||
console.log(`Only ${renderedCards.length} DOM nodes created for 1000 agents`);
|
||||
});
|
||||
|
||||
it('efficiently handles 5000 agents (stress test)', () => {
|
||||
const startTime = performance.now();
|
||||
renderComponent(5000);
|
||||
const endTime = performance.now();
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '2500'); // 5000 agents / 2 per row
|
||||
|
||||
// Should still only render visible cards
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThan(50);
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
|
||||
// Performance should still be reasonable
|
||||
const renderTime = endTime - startTime;
|
||||
expect(renderTime).toBeLessThan(200); // Should render in less than 200ms
|
||||
|
||||
console.log(`Rendered 5000 agents in ${renderTime.toFixed(2)}ms`);
|
||||
console.log(`Only ${renderedCards.length} DOM nodes created for 5000 agents`);
|
||||
});
|
||||
|
||||
it('calculates correct number of virtual rows for different screen sizes', () => {
|
||||
// Test desktop layout (2 cards per row)
|
||||
renderComponent(100);
|
||||
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toHaveAttribute('data-total-rows', '50'); // 100 agents / 2 per row
|
||||
});
|
||||
|
||||
it('row renderer is called efficiently', () => {
|
||||
// Reset the mock before testing
|
||||
mockRowRenderer.mockClear();
|
||||
|
||||
renderComponent(1000);
|
||||
|
||||
// Check that virtual list was rendered
|
||||
const virtualList = screen.getByTestId('virtual-list');
|
||||
expect(virtualList).toBeInTheDocument();
|
||||
|
||||
// With virtualization, we should only render visible rows
|
||||
// Our mock renders 10 visible rows max
|
||||
const renderedCards = screen.getAllByTestId(/agent-card-/);
|
||||
expect(renderedCards.length).toBeLessThanOrEqual(20); // At most 10 rows * 2 cards per row
|
||||
expect(renderedCards.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('memory usage remains stable with large datasets', () => {
|
||||
// Test that memory doesn't grow linearly with data size
|
||||
const measureMemory = () => {
|
||||
const cards = screen.queryAllByTestId(/agent-card-/);
|
||||
return cards.length;
|
||||
};
|
||||
|
||||
renderComponent(100);
|
||||
const memory100 = measureMemory();
|
||||
|
||||
renderComponent(1000);
|
||||
const memory1000 = measureMemory();
|
||||
|
||||
renderComponent(5000);
|
||||
const memory5000 = measureMemory();
|
||||
|
||||
// Memory usage should not scale linearly with data size
|
||||
// All should render roughly the same number of DOM nodes
|
||||
expect(Math.abs(memory100 - memory1000)).toBeLessThan(30);
|
||||
expect(Math.abs(memory1000 - memory5000)).toBeLessThan(30);
|
||||
|
||||
console.log(
|
||||
`Memory usage: 100 agents=${memory100}, 1000 agents=${memory1000}, 5000 agents=${memory5000}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue