Trollboard expandable (#1354)

* wip

* hide necessary column in trollboard

* remove console.log

* fix build

* clean up

* remove commented code
This commit is contained in:
notmd
2023-02-09 15:20:27 +07:00
committed by GitHub
parent 7c4ff73241
commit ef548edb72
7 changed files with 153 additions and 60 deletions
@@ -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<T> = ColumnDef<T> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type DataTableColumnDef<T> = ColumnDef<T, any> & {
filterable?: boolean;
span?: number | ((cell: Cell<T, unknown>) => number | undefined);
};
@@ -54,6 +64,7 @@ export type DataTableProps<T> = {
disablePrevious?: boolean;
disablePagination?: boolean;
rowProps?: TableRowProps | DataTableRowPropsCallback<T>;
getSubRows?: (row: T) => T[] | undefined;
};
export const DataTable = <T,>({
@@ -68,12 +79,21 @@ export const DataTable = <T,>({
disablePrevious,
disablePagination,
rowProps,
getSubRows,
}: DataTableProps<T>) => {
const { t } = useTranslation("leaderboard");
const [expanded, setExpanded] = useState<ExpandedState>({});
const { getHeaderGroups, getRowModel } = useReactTable<T>({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
state: {
expanded,
},
getSubRows,
onExpandedChange: setExpanded,
});
const handleFilterChange = (value: FilterItem) => {
@@ -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<T> = Omit<T, "shouldExpand"> & {
shouldExpand?: boolean;
};
export const createJsonExpandRowModel = <T,>() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderCell = ({ row, getValue }: CellContext<ExpandableRow<T>, any>) => {
if (!row.original.shouldExpand) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { shouldExpand, ...res } = row.original;
return (
<Card variant="json">
<CardBody>
<pre>{JSON.stringify(res, null, 2)}</pre>
</CardBody>
</Card>
);
}
return (
<Flex alignItems="center">
{row.getCanExpand() ? (
<button
{...{
onClick: row.getToggleExpandedHandler(),
style: { cursor: "pointer" },
}}
>
{row.getIsExpanded() ? <ChevronDown /> : <ChevronRight />}
</button>
) : null}{" "}
{getValue()}
</Flex>
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const span = (cell: Cell<ExpandableRow<T>, any>) =>
cell.row.original.shouldExpand ? undefined : cell.row.getVisibleCells().length;
const getSubRows = (row: ExpandableRow<T>) =>
row.shouldExpand
? [
{
...row,
shouldExpand: false,
},
]
: undefined;
const toExpandable = function (arr: T[] | undefined, val = true): ExpandableRow<T>[] {
return !arr ? [] : arr.map((element) => ({ ...element, shouldExpand: val }));
};
return { renderCell, span, getSubRows, toExpandable };
};
@@ -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<WindowLeaderboardEntity>();
const jsonExpandRowModel = createJsonExpandRowModel<WindowLeaderboardEntity>();
/**
* Presents a grid of leaderboard entries with more detailed information.
*/
@@ -39,17 +40,24 @@ export const LeaderboardTable = ({
} = useFetchBoard<LeaderboardReply & { user_stats_window?: LeaderboardReply["leaderboard"] }>(
`/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<WindowLeaderboardEntity>[] = useMemo(
() => [
{
...columnHelper.accessor("rank", {
header: t("rank"),
cell: ({ row, getValue }) => (row.original.isSpaceRow ? <SpaceRow></SpaceRow> : getValue()),
cell: (ctx) =>
ctx.row.original.isSpaceRow ? (
<SpaceRow></SpaceRow>
) : 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}
></DataTable>
);
@@ -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<TrollboardEntity>();
const toPercentage = (num: number) => `${Math.round(num * 100)}%`;
const jsonExpandRowModel = createJsonExpandRowModel<TrollboardEntity>();
const columns = [
columnHelper.accessor("rank", {}),
const columns: DataTableColumnDef<TrollboardEntity>[] = [
{
...columnHelper.accessor("rank", {
cell: jsonExpandRowModel.renderCell,
}),
span: jsonExpandRowModel.span,
},
columnHelper.accessor("display_name", {
header: "Display name",
cell: ({ getValue, row }) => (
<Link as={NextLink} href={`/admin/manage_user/${row.original.user_id}`}>
{getValue()}
</Link>
),
cell: ({ getValue, row }) => {
const isEmail = row.original.auth_method === "local";
return (
<Flex gap="2" alignItems="center">
<Link as={NextLink} href={`/admin/manage_user/${row.original.user_id}`}>
{getValue()}
</Link>
<Tooltip label={`This user signin with ${isEmail ? "email" : "discord"}`}>
{isEmail ? <Mail size="20"></Mail> : <Discord size="20"></Discord>}
</Tooltip>
</Flex>
);
},
}),
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 }) => (
<IconButton
as={NextLink}
href={`/admin/manage_user/${row.original.user_id}`}
aria-label={"View user"}
icon={<User></User>}
></IconButton>
),
}),
];
@@ -94,7 +93,11 @@ export const TrollboardTable = ({
lastUpdated,
} = useFetchBoard<FetchTrollBoardResponse>(`/api/admin/trollboard?time_frame=${timeFrame}&limit=${limit}`);
const { data, ...paginationProps } = useBoardPagination({ rowPerPage, data: trollboardRes?.trollboard, limit });
const { data, ...paginationProps } = useBoardPagination<TrollboardEntity>({
rowPerPage,
data: jsonExpandRowModel.toExpandable(trollboardRes?.trollboard),
limit,
});
const rowProps = useBoardRowProps<TrollboardEntity>();
if (isLoading) {
return <CircularProgress isIndeterminate></CircularProgress>;
@@ -112,11 +115,12 @@ export const TrollboardTable = ({
},
}}
>
<DataTable<TrollboardEntity>
<DataTable
data={data}
columns={columns}
caption={lastUpdated}
rowProps={rowProps}
getSubRows={jsonExpandRowModel.getSubRows}
{...paginationProps}
></DataTable>
</Box>
@@ -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 = <T extends { highlighted: boolean }>() => {
const borderColor = useToken("colors", useColorModeValue(colors.light.active, colors.dark.active));
+1 -1
View File
@@ -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 {
/**
+1 -1
View File
@@ -18,7 +18,7 @@ const Leaderboard = () => {
<AdminArea>
<Box display="flex" flexDirection="column">
<Heading fontSize="2xl" fontWeight="bold" pb="4">
{t("leaderboard")}
Trollboard
</Heading>
<Card>
<CardBody>