diff --git a/website/src/components/DataTable.tsx b/website/src/components/DataTable/DataTable.tsx similarity index 90% rename from website/src/components/DataTable.tsx rename to website/src/components/DataTable/DataTable.tsx index 35246bf7..3e75f32e 100644 --- a/website/src/components/DataTable.tsx +++ b/website/src/components/DataTable/DataTable.tsx @@ -23,13 +23,23 @@ import { Tr, useDisclosure, } from "@chakra-ui/react"; -import { Cell, ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table"; +import { + Cell, + ColumnDef, + ExpandedState, + flexRender, + getCoreRowModel, + getExpandedRowModel, + Row, + useReactTable, +} from "@tanstack/react-table"; import { Filter } from "lucide-react"; import { useTranslation } from "next-i18next"; -import { ChangeEvent, ReactNode } from "react"; +import { ChangeEvent, ReactNode, useState } from "react"; import { useDebouncedCallback } from "use-debounce"; -export type DataTableColumnDef = ColumnDef & { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DataTableColumnDef = ColumnDef & { filterable?: boolean; span?: number | ((cell: Cell) => number | undefined); }; @@ -54,6 +64,7 @@ export type DataTableProps = { disablePrevious?: boolean; disablePagination?: boolean; rowProps?: TableRowProps | DataTableRowPropsCallback; + getSubRows?: (row: T) => T[] | undefined; }; export const DataTable = ({ @@ -68,12 +79,21 @@ export const DataTable = ({ disablePrevious, disablePagination, rowProps, + getSubRows, }: DataTableProps) => { const { t } = useTranslation("leaderboard"); + const [expanded, setExpanded] = useState({}); + const { getHeaderGroups, getRowModel } = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: getExpandedRowModel(), + state: { + expanded, + }, + getSubRows, + onExpandedChange: setExpanded, }); const handleFilterChange = (value: FilterItem) => { diff --git a/website/src/components/DataTable/jsonExpandRowModel.tsx b/website/src/components/DataTable/jsonExpandRowModel.tsx new file mode 100644 index 00000000..84c70e20 --- /dev/null +++ b/website/src/components/DataTable/jsonExpandRowModel.tsx @@ -0,0 +1,60 @@ +import { Card, CardBody, Flex } from "@chakra-ui/react"; +import { Cell, CellContext } from "@tanstack/react-table"; +import { ChevronDown, ChevronRight } from "lucide-react"; + +type ExpandableRow = Omit & { + shouldExpand?: boolean; +}; + +export const createJsonExpandRowModel = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderCell = ({ row, getValue }: CellContext, any>) => { + if (!row.original.shouldExpand) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { shouldExpand, ...res } = row.original; + return ( + + +
{JSON.stringify(res, null, 2)}
+
+
+ ); + } + + return ( + + {row.getCanExpand() ? ( + + ) : null}{" "} + {getValue()} + + ); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const span = (cell: Cell, any>) => + cell.row.original.shouldExpand ? undefined : cell.row.getVisibleCells().length; + + const getSubRows = (row: ExpandableRow) => + row.shouldExpand + ? [ + { + ...row, + shouldExpand: false, + }, + ] + : undefined; + + const toExpandable = function (arr: T[] | undefined, val = true): ExpandableRow[] { + return !arr ? [] : arr.map((element) => ({ ...element, shouldExpand: val })); + }; + + return { renderCell, span, getSubRows, toExpandable }; +}; diff --git a/website/src/components/LeaderboardTable/LeaderboardTable.tsx b/website/src/components/LeaderboardTable/LeaderboardTable.tsx index d59fc902..fc3ae099 100644 --- a/website/src/components/LeaderboardTable/LeaderboardTable.tsx +++ b/website/src/components/LeaderboardTable/LeaderboardTable.tsx @@ -2,19 +2,20 @@ import { Box, CircularProgress, Flex, Link, useColorModeValue } from "@chakra-ui import { createColumnHelper } from "@tanstack/react-table"; import { MoreHorizontal } from "lucide-react"; import NextLink from "next/link"; -import { useSession } from "next-auth/react"; import { useTranslation } from "next-i18next"; import React, { useMemo } from "react"; +import { useHasRole } from "src/hooks/auth/useHasRole"; import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard"; -import { DataTable, DataTableColumnDef } from "../DataTable"; +import { DataTable, DataTableColumnDef } from "../DataTable/DataTable"; +import { createJsonExpandRowModel } from "../DataTable/jsonExpandRowModel"; import { useBoardPagination } from "./useBoardPagination"; import { useBoardRowProps } from "./useBoardRowProps"; import { useFetchBoard } from "./useFetchBoard"; type WindowLeaderboardEntity = LeaderboardEntity & { isSpaceRow?: boolean }; const columnHelper = createColumnHelper(); - +const jsonExpandRowModel = createJsonExpandRowModel(); /** * Presents a grid of leaderboard entries with more detailed information. */ @@ -39,17 +40,24 @@ export const LeaderboardTable = ({ } = useFetchBoard( `/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}` ); - const { data: session } = useSession(); - const isAdmin = session?.user?.role === "admin"; + const isAdmin = useHasRole("admin"); + const columns: DataTableColumnDef[] = useMemo( () => [ { ...columnHelper.accessor("rank", { header: t("rank"), - cell: ({ row, getValue }) => (row.original.isSpaceRow ? : getValue()), + cell: (ctx) => + ctx.row.original.isSpaceRow ? ( + + ) : isAdmin ? ( + jsonExpandRowModel.renderCell(ctx) + ) : ( + ctx.getValue() + ), }), - span: (cell) => (cell.row.original.isSpaceRow ? 6 : undefined), + span: (cell) => (cell.row.original.isSpaceRow ? 6 : jsonExpandRowModel.span(cell)), }, columnHelper.accessor("display_name", { header: t("user"), @@ -82,17 +90,17 @@ export const LeaderboardTable = ({ data: paginatedData, end, ...pagnationProps - } = useBoardPagination({ rowPerPage, data: reply?.leaderboard, limit }); - const data: WindowLeaderboardEntity[] = useMemo(() => { - if (hideCurrentUserRanking || !reply?.user_stats_window) { + } = useBoardPagination({ rowPerPage, data: jsonExpandRowModel.toExpandable(reply?.leaderboard || []), limit }); + const data = useMemo(() => { + if (hideCurrentUserRanking || !reply?.user_stats_window || reply.user_stats_window.length === 0) { return paginatedData; } - const userStatsWindow: WindowLeaderboardEntity[] = reply.user_stats_window; + const userStatsWindow: WindowLeaderboardEntity[] = jsonExpandRowModel.toExpandable(reply.user_stats_window); const userStats = userStatsWindow.find((stats) => stats.highlighted); if (userStats && userStats.rank > end) { paginatedData.push( { isSpaceRow: true } as WindowLeaderboardEntity, - ...reply.user_stats_window.filter( + ...userStatsWindow.filter( (stats) => paginatedData.findIndex((leaderBoardEntity) => leaderBoardEntity.user_id === stats.user_id) === -1 ) // filter to avoid duplicated row ); @@ -116,6 +124,7 @@ export const LeaderboardTable = ({ columns={columns} caption={lastUpdated} rowProps={rowProps} + getSubRows={jsonExpandRowModel.getSubRows} {...pagnationProps} > ); diff --git a/website/src/components/LeaderboardTable/TrollboardTable.tsx b/website/src/components/LeaderboardTable/TrollboardTable.tsx index 1b1ea118..0c971655 100644 --- a/website/src/components/LeaderboardTable/TrollboardTable.tsx +++ b/website/src/components/LeaderboardTable/TrollboardTable.tsx @@ -1,26 +1,42 @@ -import { Box, CircularProgress, Flex, Link } from "@chakra-ui/react"; +import { Box, CircularProgress, Flex, IconButton, Link, Tooltip } from "@chakra-ui/react"; import { createColumnHelper } from "@tanstack/react-table"; -import { ThumbsDown, ThumbsUp } from "lucide-react"; +import { Mail, ThumbsDown, ThumbsUp, User } from "lucide-react"; import NextLink from "next/link"; import { FetchTrollBoardResponse, TrollboardEntity, TrollboardTimeFrame } from "src/types/Trollboard"; -import { DataTable } from "../DataTable"; +import { DataTable, DataTableColumnDef } from "../DataTable/DataTable"; +import { createJsonExpandRowModel } from "../DataTable/jsonExpandRowModel"; +import { Discord } from "../Icons/Discord"; import { useBoardPagination } from "./useBoardPagination"; import { useBoardRowProps } from "./useBoardRowProps"; import { useFetchBoard } from "./useFetchBoard"; + const columnHelper = createColumnHelper(); - const toPercentage = (num: number) => `${Math.round(num * 100)}%`; +const jsonExpandRowModel = createJsonExpandRowModel(); -const columns = [ - columnHelper.accessor("rank", {}), +const columns: DataTableColumnDef[] = [ + { + ...columnHelper.accessor("rank", { + cell: jsonExpandRowModel.renderCell, + }), + span: jsonExpandRowModel.span, + }, columnHelper.accessor("display_name", { header: "Display name", - cell: ({ getValue, row }) => ( - - {getValue()} - - ), + cell: ({ getValue, row }) => { + const isEmail = row.original.auth_method === "local"; + return ( + + + {getValue()} + + + {isEmail ? : } + + + ); + }, }), columnHelper.accessor("troll_score", { header: "Troll score", @@ -45,36 +61,19 @@ const columns = [ columnHelper.accessor((row) => row.spam + row.spam_prompts, { header: "Spam", }), - columnHelper.accessor("lang_mismach", { - header: "Lang mismach", - }), - columnHelper.accessor("not_appropriate", { - header: "Not appropriate", - }), - columnHelper.accessor("pii", {}), - columnHelper.accessor("hate_speech", { - header: "Hate speech", - }), - columnHelper.accessor("sexual_content", { - header: "Sexual Content", - }), - columnHelper.accessor("political_content", { - header: "Political Content", - }), - columnHelper.accessor("quality", { - cell: ({ getValue }) => toPercentage(getValue()), - }), - columnHelper.accessor("helpfulness", { - cell: ({ getValue }) => toPercentage(getValue()), - }), - columnHelper.accessor("humor", { - cell: ({ getValue }) => toPercentage(getValue()), - }), - columnHelper.accessor("violence", { - cell: ({ getValue }) => toPercentage(getValue()), - }), columnHelper.accessor("toxicity", { - cell: ({ getValue }) => toPercentage(getValue()), + cell: ({ getValue }) => toPercentage(getValue() || 0), + }), + columnHelper.accessor((row) => row.user_id, { + header: "Actions", + cell: ({ row }) => ( + } + > + ), }), ]; @@ -94,7 +93,11 @@ export const TrollboardTable = ({ lastUpdated, } = useFetchBoard(`/api/admin/trollboard?time_frame=${timeFrame}&limit=${limit}`); - const { data, ...paginationProps } = useBoardPagination({ rowPerPage, data: trollboardRes?.trollboard, limit }); + const { data, ...paginationProps } = useBoardPagination({ + rowPerPage, + data: jsonExpandRowModel.toExpandable(trollboardRes?.trollboard), + limit, + }); const rowProps = useBoardRowProps(); if (isLoading) { return ; @@ -112,11 +115,12 @@ export const TrollboardTable = ({ }, }} > - + diff --git a/website/src/components/LeaderboardTable/useBoardRowProps.ts b/website/src/components/LeaderboardTable/useBoardRowProps.ts index 32f3fc56..be0fe7de 100644 --- a/website/src/components/LeaderboardTable/useBoardRowProps.ts +++ b/website/src/components/LeaderboardTable/useBoardRowProps.ts @@ -2,7 +2,7 @@ import { useColorModeValue, useToken } from "@chakra-ui/react"; import { useCallback } from "react"; import { colors } from "src/styles/Theme/colors"; -import { DataTableRowPropsCallback } from "../DataTable"; +import { DataTableRowPropsCallback } from "../DataTable/DataTable"; export const useBoardRowProps = () => { const borderColor = useToken("colors", useColorModeValue(colors.light.active, colors.dark.active)); diff --git a/website/src/components/UserTable.tsx b/website/src/components/UserTable.tsx index ab05d065..5c51c585 100644 --- a/website/src/components/UserTable.tsx +++ b/website/src/components/UserTable.tsx @@ -7,7 +7,7 @@ import { get } from "src/lib/api"; import type { FetchUsersResponse, User } from "src/types/Users"; import useSWR from "swr"; -import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable"; +import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable/DataTable"; interface Pagination { /** diff --git a/website/src/pages/admin/trollboard.tsx b/website/src/pages/admin/trollboard.tsx index aaef80f3..47e69d6b 100644 --- a/website/src/pages/admin/trollboard.tsx +++ b/website/src/pages/admin/trollboard.tsx @@ -18,7 +18,7 @@ const Leaderboard = () => { - {t("leaderboard")} + Trollboard