Skip to main content
Glama
DiscussionsAdminPage.tsx14.3 kB
'use client'; import { Link } from '@components/Link/Link'; import type { DiscussionAPI, GetDiscussionsResult, GetUsersResult, UserAPI, } from '@intlayer/backend'; import { Avatar, CopyToClipboard, Loader, Modal, NumberItemsSelector, Pagination, SearchInput, ShowingResultsNumberItems, Table, } from '@intlayer/design-system'; import { useGetDiscussions, useGetUsers, useSearch, } from '@intlayer/design-system/hooks'; import { type ColumnDef, flexRender, getCoreRowModel, type SortingState, useReactTable, } from '@tanstack/react-table'; import { cn } from '@utils/cn'; import { ChevronDown, ChevronUp, Search } from 'lucide-react'; import { useIntlayer } from 'next-intlayer'; import { type FC, useEffect, useState } from 'react'; import { useSearchParamState } from '@/hooks/useSearchParamState'; import { PagesRoutes } from '@/Routes'; import { DiscussionAdminDetail } from './DiscussionAdminDetail'; export const DiscussionsAdminPageContent: FC = () => { type SortOrder = 'asc' | 'desc'; const { params, setParam, setParams } = useSearchParamState({ page: { type: 'number', fallbackValue: 1 }, pageSize: { type: 'number', fallbackValue: 10 }, search: { type: 'string', fallbackValue: undefined }, sortBy: { type: 'string', fallbackValue: undefined }, sortOrder: { type: 'string', fallbackValue: 'asc' }, }); const { setSearch, search } = useSearch({}); const [discussionId, setDiscussionId] = useState<string | undefined>( undefined ); const discussionsQuery = useGetDiscussions({ fetchAll: 'true', includeMessages: 'false', ...(search && { search }), page: params.page.toString(), pageSize: params.pageSize.toString(), ...(params.sortBy && { sortBy: params.sortBy }), ...(params.sortOrder && { sortOrder: params.sortOrder }), ...(params.search && { search: params.search }), }); // users fetched after discussions are loaded const { data, error, isFetching } = discussionsQuery; const { title, tableHeaders, noData, errorMessages, searchPlaceholder, modalTitle, } = useIntlayer('discussion-admin-detail'); const discussionsResponse = data as GetDiscussionsResult | undefined; const discussions = discussionsResponse?.data ?? []; const userIds: string[] = Array.from( new Set( discussions .map((d) => d.userId) .filter(Boolean) .map(String) ) ); const usersQuery = useGetUsers( { ids: userIds, fetchAll: 'true' }, { enabled: userIds.length > 0 } ); const usersResponse = usersQuery.data as GetUsersResult | undefined; const users = usersResponse?.data ?? []; const userIdToUser = new Map<string, UserAPI>(); for (const user of users) userIdToUser.set(String(user.id), user); const totalPages: number = discussionsResponse?.total_pages ?? 1; const totalItems: number = discussionsResponse?.total_items ?? 0; const currentPage: number = params.page; const itemsPerPage: number = params.pageSize; const sorting: SortingState = params.sortBy ? [{ id: params.sortBy, desc: params.sortOrder === 'desc' }] : []; const columns: ColumnDef<DiscussionAPI>[] = [ { accessorKey: 'id', enableSorting: true, header: ({ column }) => ( <div className="group flex items-center gap-2"> {tableHeaders.id} <div className={cn( 'opacity-0 transition-opacity duration-300 group-hover:opacity-100', column.getIsSorted() && 'opacity-100' )} > {column.getIsSorted() === 'asc' ? ( <ChevronUp className="h-3 w-3" /> ) : column.getIsSorted() === 'desc' ? ( <ChevronDown className="h-3 w-3" /> ) : null} </div> </div> ), cell: ({ row }) => { const discussion = row.original as DiscussionAPI; return ( <div className="ml-3 font-mono text-sm"> ... {String(discussion.id).slice(-5)} </div> ); }, }, { accessorKey: 'numberOfMessages', enableSorting: true, header: ({ column }) => ( <div className="group flex items-center gap-2"> {tableHeaders.numberOfMessages.value} <div className={cn( 'opacity-0 transition-opacity duration-300 group-hover:opacity-100', column.getIsSorted() && 'opacity-100' )} > {column.getIsSorted() === 'asc' ? ( <ChevronUp className="h-3 w-3" /> ) : column.getIsSorted() === 'desc' ? ( <ChevronDown className="h-3 w-3" /> ) : null} </div> </div> ), cell: ({ row }) => { const discussion = row.original as any; return ( <div className="text-center font-medium"> {discussion.numberOfMessages ?? 0} </div> ); }, }, { accessorKey: 'userName', enableSorting: true, header: ({ column }) => ( <div className="group flex items-center gap-2"> {tableHeaders.userName.value} <div className={cn( 'opacity-0 transition-opacity duration-300 group-hover:opacity-100', column.getIsSorted() && 'opacity-100' )} > {column.getIsSorted() === 'asc' ? ( <ChevronUp className="h-3 w-3" /> ) : column.getIsSorted() === 'desc' ? ( <ChevronDown className="h-3 w-3" /> ) : null} </div> </div> ), cell: ({ row }) => { const discussion = row.original as DiscussionAPI; const user = userIdToUser.get(String(discussion.userId)); return ( <div className="flex items-center"> <Avatar isLoggedIn={true} isLoading={usersQuery.isFetching} className="shrink-0" src={user?.image ?? undefined} fullname={user?.name ?? ''} /> {user?.id && ( <div className="ml-3"> {user?.name ? ( <Link href={PagesRoutes.Admin_Users_Id.replace(':id', user.id)} label={user.name ?? '-'} color="text" > <CopyToClipboard text={user.name}> {user.name} </CopyToClipboard> </Link> ) : ( '-' )} </div> )} </div> ); }, }, { accessorKey: 'createdAt', enableSorting: true, header: ({ column }) => ( <div className="group flex items-center gap-2"> {tableHeaders.createdAt.value} <div className={cn( 'opacity-0 transition-opacity duration-300 group-hover:opacity-100', column.getIsSorted() && 'opacity-100' )} > {column.getIsSorted() === 'asc' ? ( <ChevronUp className="h-3 w-3" /> ) : column.getIsSorted() === 'desc' ? ( <ChevronDown className="h-3 w-3" /> ) : null} </div> </div> ), cell: ({ row }) => { const discussion = row.original as DiscussionAPI; return ( <div className="text-neutral-500 text-sm dark:text-neutral-400"> {discussion.createdAt ? new Date(discussion.createdAt).toLocaleDateString() : noData.value} </div> ); }, }, { accessorKey: 'updatedAt', enableSorting: true, header: ({ column }) => ( <div className="group flex items-center gap-2"> {tableHeaders.updatedAt} <div className={cn( 'opacity-0 transition-opacity duration-300 group-hover:opacity-100', column.getIsSorted() && 'opacity-100' )} > {column.getIsSorted() === 'asc' ? ( <ChevronUp className="h-3 w-3" /> ) : column.getIsSorted() === 'desc' ? ( <ChevronDown className="h-3 w-3" /> ) : null} </div> </div> ), cell: ({ row }) => { const discussion = row.original as DiscussionAPI; return ( <div className="text-neutral-500 text-sm dark:text-neutral-400"> {discussion.updatedAt ? new Date(discussion.updatedAt).toLocaleDateString() : noData.value} </div> ); }, }, ]; const table = useReactTable({ data: discussions, columns, state: { sorting }, manualSorting: true, onSortingChange: (updater) => { const next = typeof updater === 'function' ? updater(sorting) : updater; if (next.length > 0) { const s = next[0]; const field = s.id; const order: SortOrder = s.desc ? 'desc' : 'asc'; setParams({ sortBy: field, sortOrder: order, page: 1 }); } else { setParams({ sortBy: '', sortOrder: 'asc', page: 1 }); } }, getCoreRowModel: getCoreRowModel(), }); const handleSearch = (value: string) => { setSearch(value); setParams({ search: value, page: 1 }); }; // Keep the input's search value in sync with URL param useEffect(() => { setSearch((params.search as string) ?? ''); }, [params.search, setSearch]); const handlePageChange = (page: number) => { setParam('page', page); }; const handlePageSizeChange = (newPageSize: string) => { const size = parseInt(newPageSize, 10); setParams({ pageSize: size, page: 1 }); }; if (error) { return ( <div className="p-6"> <div className="text-error"> {errorMessages.loadingError}:{' '} {error instanceof Error ? error.message : String(error)} </div> </div> ); } return ( <div className="flex flex-1 flex-col"> <div className="mb-6"> <h1 className="font-bold text-2xl text-neutral-900 dark:text-neutral-100"> {title} </h1> </div> <div className="mb-4 space-y-4"> <div className="flex flex-col gap-4 sm:flex-row"> <div className="relative flex-1"> <Search className="-translate-y-1/2 absolute top-1/2 left-3 h-4 w-4 text-muted-foreground" /> <SearchInput placeholder={searchPlaceholder.value} onChange={(e) => handleSearch(e.target.value)} className="max-w-md pl-10" /> </div> </div> </div> <Loader isLoading={isFetching}> {discussions.length === 0 ? ( <div className="py-12 text-center"> <p className="text-neutral-500 dark:text-neutral-400"> {noData.value} </p> </div> ) : ( <div className="space-y-4"> <Table className="w-full"> <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id} className="border-neutral-200 border-b dark:border-neutral-700" > {headerGroup.headers.map((header) => ( <th key={header.id} className={cn( 'whitespace-nowrap px-4 py-3 text-left font-medium text-neutral-900 dark:text-neutral-100', header.column.getCanSort() && 'cursor-pointer select-none hover:text-neutral-600' )} onClick={ header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined } > {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map((row) => ( <tr key={row.id} className="cursor-pointer whitespace-nowrap border-neutral-100 border-b hover:bg-neutral-50 dark:border-neutral-800 dark:hover:bg-neutral-800" > {row.getVisibleCells().map((cell) => ( <td key={cell.id} className="whitespace-nowrap px-4 py-3" onClick={() => setDiscussionId(String(row.original.id))} > {flexRender( cell.column.columnDef.cell, cell.getContext() )} </td> ))} </tr> ))} </tbody> </Table> </div> )} </Loader> <div className="flex w-full flex-row items-end justify-between gap-4 pt-8"> <div className="flex flex-col gap-4"> <ShowingResultsNumberItems currentPage={currentPage} pageSize={itemsPerPage} totalItems={totalItems} /> <NumberItemsSelector value={itemsPerPage.toString()} onValueChange={handlePageSizeChange} /> </div> <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} /> </div> <Modal isOpen={!!discussionId} onClose={() => setDiscussionId(undefined)} title={modalTitle({ discussionId: discussionId ?? '' }).value} size="xl" hasCloseButton > <DiscussionAdminDetail discussionId={discussionId} /> </Modal> </div> ); }; export default DiscussionsAdminPageContent;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server