🖼️ 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:
Marco Beretta 2025-08-04 18:50:54 +02:00 committed by Danny Avila
parent 9585db14ba
commit d82a63642d
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
51 changed files with 2074 additions and 1311 deletions

View file

@ -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}`,
);
});
});