mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
🔐 feat: Granular Role-based Permissions + Entra ID Group Discovery (#7804)
WIP: pre-granular-permissions commit
feat: Add category and support contact fields to Agent schema and UI components
Revert "feat: Add category and support contact fields to Agent schema and UI components"
This reverts commit c43a52b4c9.
Fix: Update import for renderHook in useAgentCategories.spec.tsx
fix: Update icon rendering in AgentCategoryDisplay tests to use empty spans
refactor: Improve category synchronization logic and clean up AgentConfig component
refactor: Remove unused UI flow translations from translation.json
feat: agent marketplace features
🔐 feat: Granular Role-based Permissions + Entra ID Group Discovery (#7804)
This commit is contained in:
parent
aa42759ffd
commit
66bd419baa
147 changed files with 17564 additions and 645 deletions
|
|
@ -0,0 +1,35 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import useAgentCategories from '../useAgentCategories';
|
||||
import { AGENT_CATEGORIES, EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories';
|
||||
|
||||
// Mock the useLocalize hook
|
||||
jest.mock('~/hooks/useLocalize', () => ({
|
||||
__esModule: true,
|
||||
default: () => (key: string) => {
|
||||
// Simple mock implementation that returns the key as the translation
|
||||
return key === 'com_ui_agent_category_general' ? 'General (Translated)' : key;
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useAgentCategories', () => {
|
||||
it('should return processed categories with correct structure', () => {
|
||||
const { result } = renderHook(() => useAgentCategories());
|
||||
|
||||
// Check that we have the expected number of categories
|
||||
expect(result.current.categories.length).toBe(AGENT_CATEGORIES.length);
|
||||
|
||||
// Check that the first category has the expected structure
|
||||
const firstCategory = result.current.categories[0];
|
||||
const firstOriginalCategory = AGENT_CATEGORIES[0];
|
||||
|
||||
expect(firstCategory.value).toBe(firstOriginalCategory.value);
|
||||
|
||||
// Check that labels are properly translated
|
||||
expect(firstCategory.label).toBe('General (Translated)');
|
||||
expect(firstCategory.className).toBe('w-full');
|
||||
|
||||
// Check the empty category
|
||||
expect(result.current.emptyCategory.value).toBe(EMPTY_AGENT_CATEGORY.value);
|
||||
expect(result.current.emptyCategory.label).toBeTruthy();
|
||||
});
|
||||
});
|
||||
360
client/src/hooks/Agents/__tests__/useDynamicAgentQuery.spec.ts
Normal file
360
client/src/hooks/Agents/__tests__/useDynamicAgentQuery.spec.ts
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
import { renderHook } from '@testing-library/react';
|
||||
import { useDynamicAgentQuery } from '../useDynamicAgentQuery';
|
||||
import {
|
||||
useGetPromotedAgentsQuery,
|
||||
useGetAgentsByCategoryQuery,
|
||||
useSearchAgentsQuery,
|
||||
} from '~/data-provider';
|
||||
|
||||
// Mock the data provider queries
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetPromotedAgentsQuery: jest.fn(),
|
||||
useGetAgentsByCategoryQuery: jest.fn(),
|
||||
useSearchAgentsQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseGetPromotedAgentsQuery = useGetPromotedAgentsQuery as jest.MockedFunction<
|
||||
typeof useGetPromotedAgentsQuery
|
||||
>;
|
||||
const mockUseGetAgentsByCategoryQuery = useGetAgentsByCategoryQuery as jest.MockedFunction<
|
||||
typeof useGetAgentsByCategoryQuery
|
||||
>;
|
||||
const mockUseSearchAgentsQuery = useSearchAgentsQuery as jest.MockedFunction<
|
||||
typeof useSearchAgentsQuery
|
||||
>;
|
||||
|
||||
describe('useDynamicAgentQuery', () => {
|
||||
const defaultMockQueryResult = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set default mock returns
|
||||
mockUseGetPromotedAgentsQuery.mockReturnValue(defaultMockQueryResult as any);
|
||||
mockUseGetAgentsByCategoryQuery.mockReturnValue(defaultMockQueryResult as any);
|
||||
mockUseSearchAgentsQuery.mockReturnValue(defaultMockQueryResult as any);
|
||||
});
|
||||
|
||||
describe('Search Query Type', () => {
|
||||
it('should use search query when searchQuery is provided', () => {
|
||||
const mockSearchResult = {
|
||||
...defaultMockQueryResult,
|
||||
data: { agents: [], pagination: { hasMore: false } },
|
||||
};
|
||||
mockUseSearchAgentsQuery.mockReturnValue(mockSearchResult as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'hr',
|
||||
searchQuery: 'test search',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should call search query with correct parameters
|
||||
expect(mockUseSearchAgentsQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
q: 'test search',
|
||||
category: 'hr',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
},
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
staleTime: 120000,
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should return search query result
|
||||
expect(result.current.data).toBe(mockSearchResult.data);
|
||||
expect(result.current.queryType).toBe('search');
|
||||
});
|
||||
|
||||
it('should not include category in search when category is "all" or "promoted"', () => {
|
||||
renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'all',
|
||||
searchQuery: 'test search',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockUseSearchAgentsQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
q: 'test search',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
// No category parameter should be included
|
||||
},
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Promoted Query Type', () => {
|
||||
it('should use promoted query when category is "promoted" and no search', () => {
|
||||
const mockPromotedResult = {
|
||||
...defaultMockQueryResult,
|
||||
data: { agents: [], pagination: { hasMore: false } },
|
||||
};
|
||||
mockUseGetPromotedAgentsQuery.mockReturnValue(mockPromotedResult as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'promoted',
|
||||
searchQuery: '',
|
||||
page: 2,
|
||||
limit: 8,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should call promoted query with correct parameters (no showAll)
|
||||
expect(mockUseGetPromotedAgentsQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
page: 2,
|
||||
limit: 8,
|
||||
},
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.data).toBe(mockPromotedResult.data);
|
||||
expect(result.current.queryType).toBe('promoted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('All Agents Query Type', () => {
|
||||
it('should use promoted query with showAll when category is "all" and no search', () => {
|
||||
const mockAllResult = {
|
||||
...defaultMockQueryResult,
|
||||
data: { agents: [], pagination: { hasMore: false } },
|
||||
};
|
||||
|
||||
// Mock the second call to useGetPromotedAgentsQuery (for "all" category)
|
||||
mockUseGetPromotedAgentsQuery
|
||||
.mockReturnValueOnce(defaultMockQueryResult as any) // First call for promoted
|
||||
.mockReturnValueOnce(mockAllResult as any); // Second call for all
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'all',
|
||||
searchQuery: '',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should call promoted query with showAll parameter
|
||||
expect(mockUseGetPromotedAgentsQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
page: 1,
|
||||
limit: 6,
|
||||
showAll: 'true',
|
||||
},
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.queryType).toBe('all');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Category Query Type', () => {
|
||||
it('should use category query for specific categories', () => {
|
||||
const mockCategoryResult = {
|
||||
...defaultMockQueryResult,
|
||||
data: { agents: [], pagination: { hasMore: false } },
|
||||
};
|
||||
mockUseGetAgentsByCategoryQuery.mockReturnValue(mockCategoryResult as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'finance',
|
||||
searchQuery: '',
|
||||
page: 3,
|
||||
limit: 10,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockUseGetAgentsByCategoryQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
category: 'finance',
|
||||
page: 3,
|
||||
limit: 10,
|
||||
},
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.data).toBe(mockCategoryResult.data);
|
||||
expect(result.current.queryType).toBe('category');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Configuration', () => {
|
||||
it('should apply correct query configuration to all queries', () => {
|
||||
renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'hr',
|
||||
searchQuery: '',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
const expectedConfig = expect.objectContaining({
|
||||
staleTime: 120000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: 1,
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
expect(mockUseGetAgentsByCategoryQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expectedConfig,
|
||||
);
|
||||
});
|
||||
|
||||
it('should enable only the correct query based on query type', () => {
|
||||
renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'hr',
|
||||
searchQuery: '',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
// Category query should be enabled
|
||||
expect(mockUseGetAgentsByCategoryQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ enabled: true }),
|
||||
);
|
||||
|
||||
// Other queries should be disabled
|
||||
expect(mockUseSearchAgentsQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ enabled: false }),
|
||||
);
|
||||
|
||||
expect(mockUseGetPromotedAgentsQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ enabled: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default Parameters', () => {
|
||||
it('should use default page and limit when not provided', () => {
|
||||
renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'general',
|
||||
searchQuery: '',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockUseGetAgentsByCategoryQuery).toHaveBeenCalledWith(
|
||||
{
|
||||
category: 'general',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
},
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Return Values', () => {
|
||||
it('should return all necessary query properties', () => {
|
||||
const mockResult = {
|
||||
data: { agents: [{ id: '1', name: 'Test Agent' }] },
|
||||
isLoading: true,
|
||||
error: null,
|
||||
isFetching: false,
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
||||
mockUseGetAgentsByCategoryQuery.mockReturnValue(mockResult as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'it',
|
||||
searchQuery: '',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
data: mockResult.data,
|
||||
isLoading: mockResult.isLoading,
|
||||
error: mockResult.error,
|
||||
isFetching: mockResult.isFetching,
|
||||
refetch: mockResult.refetch,
|
||||
queryType: 'category',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty search query as no search', () => {
|
||||
renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'promoted',
|
||||
searchQuery: '', // Empty string should not trigger search
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should use promoted query, not search query
|
||||
expect(mockUseGetPromotedAgentsQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ enabled: true }),
|
||||
);
|
||||
|
||||
expect(mockUseSearchAgentsQuery).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ enabled: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to promoted query for unknown query types', () => {
|
||||
const mockPromotedResult = {
|
||||
...defaultMockQueryResult,
|
||||
data: { agents: [] },
|
||||
};
|
||||
mockUseGetPromotedAgentsQuery.mockReturnValue(mockPromotedResult as any);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useDynamicAgentQuery({
|
||||
category: 'unknown-category',
|
||||
searchQuery: '',
|
||||
page: 1,
|
||||
limit: 6,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should determine this as 'category' type and use category query
|
||||
expect(result.current.queryType).toBe('category');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
export { default as useAgentsMap } from './useAgentsMap';
|
||||
export { default as useSelectAgent } from './useSelectAgent';
|
||||
export { default as useAgentCategories } from './useAgentCategories';
|
||||
export { useDynamicAgentQuery } from './useDynamicAgentQuery';
|
||||
export type { ProcessedAgentCategory } from './useAgentCategories';
|
||||
export { default as useAgentCapabilities } from './useAgentCapabilities';
|
||||
export { default as useGetAgentsConfig } from './useGetAgentsConfig';
|
||||
|
|
|
|||
57
client/src/hooks/Agents/useAgentCategories.tsx
Normal file
57
client/src/hooks/Agents/useAgentCategories.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useGetAgentCategoriesQuery } from '~/data-provider';
|
||||
import { EMPTY_AGENT_CATEGORY } from '~/constants/agentCategories';
|
||||
|
||||
// This interface matches the structure used by the ControlCombobox component
|
||||
export interface ProcessedAgentCategory {
|
||||
label: string; // Translated label
|
||||
value: string; // Category value
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook that provides processed and translated agent categories from API
|
||||
*
|
||||
* @returns Object containing categories, emptyCategory, and loading state
|
||||
*/
|
||||
const useAgentCategories = () => {
|
||||
const localize = useLocalize();
|
||||
|
||||
// Fetch categories from API
|
||||
const categoriesQuery = useGetAgentCategoriesQuery({
|
||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
const categories = useMemo((): ProcessedAgentCategory[] => {
|
||||
if (!categoriesQuery.data) return [];
|
||||
|
||||
// Filter out special categories (promoted, all) and convert to form format
|
||||
return categoriesQuery.data
|
||||
.filter((category) => category.value !== 'promoted' && category.value !== 'all')
|
||||
.map((category) => ({
|
||||
label: category.label || category.value,
|
||||
value: category.value,
|
||||
className: 'w-full',
|
||||
}));
|
||||
}, [categoriesQuery.data]);
|
||||
|
||||
const emptyCategory = useMemo(
|
||||
(): ProcessedAgentCategory => ({
|
||||
label: localize(EMPTY_AGENT_CATEGORY.label),
|
||||
value: EMPTY_AGENT_CATEGORY.value,
|
||||
className: 'w-full',
|
||||
}),
|
||||
[localize],
|
||||
);
|
||||
|
||||
return {
|
||||
categories,
|
||||
emptyCategory,
|
||||
isLoading: categoriesQuery.isLoading,
|
||||
error: categoriesQuery.error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAgentCategories;
|
||||
112
client/src/hooks/Agents/useDynamicAgentQuery.ts
Normal file
112
client/src/hooks/Agents/useDynamicAgentQuery.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
|
||||
import {
|
||||
useGetPromotedAgentsQuery,
|
||||
useGetAgentsByCategoryQuery,
|
||||
useSearchAgentsQuery,
|
||||
} from '~/data-provider';
|
||||
|
||||
interface UseDynamicAgentQueryParams {
|
||||
category: string;
|
||||
searchQuery: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single dynamic query hook that replaces 4 separate conditional queries
|
||||
* Determines the appropriate query based on category and search state
|
||||
*/
|
||||
export const useDynamicAgentQuery = ({
|
||||
category,
|
||||
searchQuery,
|
||||
page = 1,
|
||||
limit = 6,
|
||||
}: UseDynamicAgentQueryParams) => {
|
||||
// Shared query configuration optimized to prevent unnecessary loading states
|
||||
const queryConfig: UseQueryOptions<t.AgentListResponse> = useMemo(
|
||||
() => ({
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes - agents don't change frequently
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
retry: 1,
|
||||
keepPreviousData: true,
|
||||
// Removed placeholderData due to TypeScript compatibility - keepPreviousData is sufficient
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Determine query type and parameters based on current state
|
||||
const queryType = useMemo(() => {
|
||||
if (searchQuery) return 'search';
|
||||
if (category === 'promoted') return 'promoted';
|
||||
if (category === 'all') return 'all';
|
||||
return 'category';
|
||||
}, [category, searchQuery]);
|
||||
|
||||
// Search query - when user is searching
|
||||
const searchQuery_result = useSearchAgentsQuery(
|
||||
{
|
||||
q: searchQuery,
|
||||
...(category !== 'all' && category !== 'promoted' && { category }),
|
||||
page,
|
||||
limit,
|
||||
},
|
||||
{
|
||||
...queryConfig,
|
||||
enabled: queryType === 'search',
|
||||
},
|
||||
);
|
||||
|
||||
// Promoted agents query - for "Top Picks" tab
|
||||
const promotedQuery = useGetPromotedAgentsQuery(
|
||||
{ page, limit },
|
||||
{
|
||||
...queryConfig,
|
||||
enabled: queryType === 'promoted',
|
||||
},
|
||||
);
|
||||
|
||||
// All agents query - for "All" tab (promoted endpoint with showAll parameter)
|
||||
const allAgentsQuery = useGetPromotedAgentsQuery(
|
||||
{ page, limit, showAll: 'true' },
|
||||
{
|
||||
...queryConfig,
|
||||
enabled: queryType === 'all',
|
||||
},
|
||||
);
|
||||
|
||||
// Category-specific query - for individual categories
|
||||
const categoryQuery = useGetAgentsByCategoryQuery(
|
||||
{ category, page, limit },
|
||||
{
|
||||
...queryConfig,
|
||||
enabled: queryType === 'category',
|
||||
},
|
||||
);
|
||||
|
||||
// Return the active query based on current state
|
||||
const activeQuery = useMemo(() => {
|
||||
switch (queryType) {
|
||||
case 'search':
|
||||
return searchQuery_result;
|
||||
case 'promoted':
|
||||
return promotedQuery;
|
||||
case 'all':
|
||||
return allAgentsQuery;
|
||||
case 'category':
|
||||
return categoryQuery;
|
||||
default:
|
||||
return promotedQuery; // fallback
|
||||
}
|
||||
}, [queryType, searchQuery_result, promotedQuery, allAgentsQuery, categoryQuery]);
|
||||
|
||||
return {
|
||||
...activeQuery,
|
||||
queryType, // Expose query type for debugging/logging
|
||||
};
|
||||
};
|
||||
|
|
@ -125,8 +125,7 @@ export const useEndpoints = ({
|
|||
if (ep === EModelEndpoint.agents && agents.length > 0) {
|
||||
result.models = agents.map((agent) => ({
|
||||
name: agent.id,
|
||||
isGlobal:
|
||||
(instanceProjectId != null && agent.projectIds?.includes(instanceProjectId)) ?? false,
|
||||
isGlobal: agent.isPublic ?? false,
|
||||
}));
|
||||
result.agentNames = agents.reduce((acc, agent) => {
|
||||
acc[agent.id] = agent.name || '';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Blocks, MCPIcon, AttachmentIcon } from '@librechat/client';
|
||||
import { MessageSquareQuote, ArrowRightToLine, Settings2, Database, Bookmark } from 'lucide-react';
|
||||
import { Database, Bookmark, Settings2, ArrowRightToLine, MessageSquareQuote } from 'lucide-react';
|
||||
import {
|
||||
isAssistantsEndpoint,
|
||||
isAgentsEndpoint,
|
||||
Permissions,
|
||||
EModelEndpoint,
|
||||
PermissionTypes,
|
||||
isParamEndpoint,
|
||||
EModelEndpoint,
|
||||
Permissions,
|
||||
isAgentsEndpoint,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import type { TInterfaceConfig, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { NavLink } from '~/common';
|
||||
|
|
|
|||
|
|
@ -31,3 +31,4 @@ export { default as useDocumentTitle } from './useDocumentTitle';
|
|||
export { default as useSpeechToText } from './Input/useSpeechToText';
|
||||
export { default as useTextToSpeech } from './Input/useTextToSpeech';
|
||||
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
|
||||
export { useResourcePermissions } from './useResourcePermissions';
|
||||
|
|
|
|||
25
client/src/hooks/useResourcePermissions.ts
Normal file
25
client/src/hooks/useResourcePermissions.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import {
|
||||
useGetEffectivePermissionsQuery,
|
||||
hasPermissions,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
|
||||
/**
|
||||
* fetches resource permissions once and returns a function to check any permission
|
||||
* More efficient when checking multiple permissions for the same resource
|
||||
* @param resourceType - Type of resource (e.g., 'agent')
|
||||
* @param resourceId - ID of the resource
|
||||
* @returns Object with hasPermission function and loading state
|
||||
*/
|
||||
export const useResourcePermissions = (resourceType: string, resourceId: string) => {
|
||||
const { data, isLoading } = useGetEffectivePermissionsQuery(resourceType, resourceId);
|
||||
|
||||
const hasPermission = (requiredPermission: number): boolean => {
|
||||
return data ? hasPermissions(data.permissionBits, requiredPermission) : false;
|
||||
};
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
isLoading,
|
||||
permissionBits: data?.permissionBits || 0,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue