mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-07-03 17:10:10 +08:00
Admin user management (#1393)
part of #1022 Allow updating the show_on_leaderboard field. Add raw JSON of the user object. Add a new user message table. Also fixed style issue: hard to see reaction count when the user also reacted. Rename MessageTable to MessageConversation.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { forwardRef, IconButton, IconButtonProps } from "@chakra-ui/react";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
export type DataTableActionProps = Omit<IconButtonProps, "icon" | "size"> & { icon: LucideIcon };
|
||||
|
||||
// need to use forwardRef from Charka to support `as` props
|
||||
// https://chakra-ui.com/community/recipes/as-prop
|
||||
export const DataTableAction = forwardRef<DataTableActionProps, "button">((props: DataTableActionProps, ref) => {
|
||||
return <IconButton size="sm" {...props} icon={<props.icon size="20"></props.icon>} ref={ref} />;
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export interface CursorPaginationState {
|
||||
/**
|
||||
* The user's `display_name` used for pagination.
|
||||
*/
|
||||
cursor: string;
|
||||
|
||||
/**
|
||||
* The pagination direction.
|
||||
*/
|
||||
direction: "forward" | "back";
|
||||
}
|
||||
|
||||
export const useCursorPagination = () => {
|
||||
const [pagination, setPagination] = useState<CursorPaginationState>({ cursor: "", direction: "forward" });
|
||||
|
||||
const toPreviousPage = (data: undefined | { prev?: string; next?: string }) => {
|
||||
setPagination({
|
||||
cursor: data?.prev || "",
|
||||
direction: "back",
|
||||
});
|
||||
};
|
||||
|
||||
const toNextPage = (data: undefined | { prev?: string; next?: string }) => {
|
||||
setPagination({
|
||||
cursor: data?.next || "",
|
||||
direction: "forward",
|
||||
});
|
||||
};
|
||||
|
||||
const resetCursor = () => setPagination((old) => ({ ...old, cursor: "" }));
|
||||
|
||||
return {
|
||||
pagination,
|
||||
toNextPage,
|
||||
toPreviousPage,
|
||||
resetCursor,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Card, CardBody } from "@chakra-ui/card";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const JsonCard = ({ children }: { children: any }) => {
|
||||
return (
|
||||
<Card variant="json">
|
||||
<CardBody overflowX="auto">
|
||||
<pre>{JSON.stringify(children, null, 2)}</pre>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Avatar } from "@chakra-ui/avatar";
|
||||
import { Badge, Flex } from "@chakra-ui/layout";
|
||||
import { Tooltip } from "@chakra-ui/react";
|
||||
import { createColumnHelper } from "@tanstack/table-core";
|
||||
import { formatDistanceToNow, formatISO9075 } from "date-fns";
|
||||
import { Eye } from "lucide-react";
|
||||
import NextLink from "next/link";
|
||||
import { ROUTES } from "src/lib/routes";
|
||||
import { Message } from "src/types/Conversation";
|
||||
import { isKnownEmoji } from "src/types/Emoji";
|
||||
import { StrictOmit } from "src/types/utils";
|
||||
|
||||
import { DataTable, DataTableProps } from "../DataTable/DataTable";
|
||||
import { DataTableAction } from "../DataTable/DataTableAction";
|
||||
import { MessageEmojiButton } from "./MessageEmojiButton";
|
||||
|
||||
const columnHelper = createColumnHelper<Message>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("text", {
|
||||
cell: ({ getValue, row }) => {
|
||||
const limit = 80;
|
||||
const text = getValue();
|
||||
const renderText = text.length > limit ? `${text.slice(0, limit)}...` : text;
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
<Avatar
|
||||
size="xs"
|
||||
mr="2"
|
||||
src={`${row.original.is_assistant ? "/images/logos/logo.png" : "/images/temp-avatars/av1.jpg"}`}
|
||||
></Avatar>
|
||||
{renderText}
|
||||
{row.original.deleted && (
|
||||
<Badge colorScheme="red" ml="1">
|
||||
Deleted
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("lang", {
|
||||
header: "Language",
|
||||
cell: ({ getValue }) => <Badge>{getValue()}</Badge>,
|
||||
}),
|
||||
columnHelper.accessor("emojis", {
|
||||
header: "Reactions",
|
||||
cell: ({ getValue, row }) => {
|
||||
const emojis = getValue();
|
||||
|
||||
emojis["+1"] = emojis["+1"] || 0;
|
||||
emojis["-1"] = emojis["-1"] || 0;
|
||||
|
||||
return (
|
||||
<Flex gap="2">
|
||||
{Object.entries(emojis)
|
||||
.filter(([emoji]) => isKnownEmoji(emoji))
|
||||
.sort(([emoji]) => -emoji)
|
||||
.map(([emoji, count]) => {
|
||||
return (
|
||||
<MessageEmojiButton
|
||||
key={emoji}
|
||||
emoji={{ name: emoji, count }}
|
||||
checked={row.original.user_emojis.includes(emoji)}
|
||||
userReacted={false}
|
||||
userIsAuthor={false}
|
||||
sx={{
|
||||
":disabled": {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("created_date", {
|
||||
header: "Date",
|
||||
cell: ({ getValue }) => {
|
||||
return <DateDiff>{getValue()}</DateDiff>;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor((row) => row.id, {
|
||||
header: "Actions",
|
||||
cell: ({ getValue }) => (
|
||||
<DataTableAction
|
||||
as={NextLink}
|
||||
href={ROUTES.ADMIN_MESSAGE_DETAIL(getValue())}
|
||||
icon={Eye}
|
||||
aria-label="View message"
|
||||
/>
|
||||
),
|
||||
}),
|
||||
];
|
||||
// TODO move this to somewhere
|
||||
const DateDiff = ({ children }: { children: string | Date | number }) => {
|
||||
const date = new Date(children);
|
||||
const diff = formatDistanceToNow(date, { addSuffix: true });
|
||||
return (
|
||||
<Tooltip label={formatISO9075(date)} placement="top">
|
||||
{diff}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const AdminMessageTable = (props: StrictOmit<DataTableProps<Message>, "columns">) => {
|
||||
return <DataTable columns={columns} {...props}></DataTable>;
|
||||
};
|
||||
+3
-3
@@ -2,12 +2,12 @@ import { SessionProvider } from "next-auth/react";
|
||||
import React from "react";
|
||||
import { Message } from "src/types/Conversation";
|
||||
|
||||
import { MessageTable } from "./MessageTable";
|
||||
import { MessageConversation } from "./MessageConversation";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
title: "Messages/MessageTable",
|
||||
component: MessageTable,
|
||||
component: MessageConversation,
|
||||
};
|
||||
|
||||
const Template = ({
|
||||
@@ -21,7 +21,7 @@ const Template = ({
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<MessageTable messages={messages} enableLink={enableLink} highlightLastMessage={highlightLastMessage} />;
|
||||
<MessageConversation messages={messages} enableLink={enableLink} highlightLastMessage={highlightLastMessage} />;
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -2,13 +2,13 @@ import { Stack } from "@chakra-ui/react";
|
||||
import { MessageTableEntry } from "src/components/Messages/MessageTableEntry";
|
||||
import { Message } from "src/types/Conversation";
|
||||
|
||||
interface MessageTableProps {
|
||||
interface MessageConversationProps {
|
||||
messages: Message[];
|
||||
enableLink?: boolean;
|
||||
highlightLastMessage?: boolean;
|
||||
}
|
||||
|
||||
export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) {
|
||||
export function MessageConversation({ messages, enableLink, highlightLastMessage }: MessageConversationProps) {
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
{messages.map((message, idx) => (
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@chakra-ui/react";
|
||||
import { Button, ButtonProps } from "@chakra-ui/react";
|
||||
import { useHasRole } from "src/hooks/auth/useHasRole";
|
||||
import { MessageEmoji } from "src/types/Conversation";
|
||||
import { emojiIcons } from "src/types/Emoji";
|
||||
@@ -6,10 +6,11 @@ import { emojiIcons } from "src/types/Emoji";
|
||||
interface MessageEmojiButtonProps {
|
||||
emoji: MessageEmoji;
|
||||
checked?: boolean;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
userIsAuthor: boolean;
|
||||
disabled?: boolean;
|
||||
userReacted: boolean;
|
||||
sx?: ButtonProps["sx"];
|
||||
}
|
||||
|
||||
export const MessageEmojiButton = ({
|
||||
@@ -19,6 +20,7 @@ export const MessageEmojiButton = ({
|
||||
userIsAuthor,
|
||||
disabled,
|
||||
userReacted,
|
||||
sx,
|
||||
}: MessageEmojiButtonProps) => {
|
||||
const EmojiIcon = emojiIcons.get(emoji.name);
|
||||
const isAdmin = useHasRole("admin");
|
||||
@@ -42,8 +44,9 @@ export const MessageEmojiButton = ({
|
||||
":hover": {
|
||||
backgroundColor: isDisabled ? "transparent" : undefined,
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
color={isDisabled ? "gray.500" : undefined}
|
||||
color={isDisabled ? (checked ? "gray.700" : "gray.500") : undefined}
|
||||
>
|
||||
<EmojiIcon style={{ height: "1em" }} />
|
||||
{showCount && <span style={{ marginInlineEnd: "0.25em" }}>{emoji.count}</span>}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { MessageConversation } from "src/components/Messages/MessageConversation";
|
||||
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
|
||||
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||
@@ -44,7 +44,7 @@ export const CreateTask = ({
|
||||
<TaskHeader taskType={taskType} />
|
||||
{task.type !== TaskType.initial_prompt && (
|
||||
<Box mt="4" borderRadius="lg" bg={cardColor} className="p-3 sm:p-6">
|
||||
<MessageTable messages={task.conversation.messages} highlightLastMessage />
|
||||
<MessageConversation messages={task.conversation.messages} highlightLastMessage />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { MessageConversation } from "src/components/Messages/MessageConversation";
|
||||
import { Sortable } from "src/components/Sortable/Sortable";
|
||||
import { SurveyCard } from "src/components/Survey/SurveyCard";
|
||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||
@@ -47,7 +47,7 @@ export const EvaluateTask = ({
|
||||
<SurveyCard>
|
||||
<TaskHeader taskType={taskType} />
|
||||
<Box mt="4" p="6" borderRadius="lg" bg={cardColor}>
|
||||
<MessageTable messages={messages} highlightLastMessage />
|
||||
<MessageConversation messages={messages} highlightLastMessage />
|
||||
</Box>
|
||||
<Sortable
|
||||
items={task[sortables]}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, useBoolean, useColorModeValue } from "@chakra-ui/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LabelInputGroup } from "src/components/Messages/LabelInputGroup";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { MessageConversation } from "src/components/Messages/MessageConversation";
|
||||
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||
import { TaskHeader } from "src/components/Tasks/TaskHeader";
|
||||
@@ -57,7 +57,7 @@ export const LabelTask = ({
|
||||
<>
|
||||
<TaskHeader taskType={taskType} />
|
||||
<Box mt="4" p={[4, 6]} borderRadius="lg" bg={cardColor}>
|
||||
<MessageTable messages={task.conversation.messages} highlightLastMessage />
|
||||
<MessageConversation messages={task.conversation.messages} highlightLastMessage />
|
||||
</Box>
|
||||
</>
|
||||
<LabelInputGroup
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Box, CircularProgress, useColorModeValue } from "@chakra-ui/react";
|
||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
||||
import { get } from "src/lib/api";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface UserMessagesCellProps {
|
||||
/**
|
||||
* The Web API route to fetch user messages from. By default is
|
||||
* `/api/messages/users` and fetches the logged in user's messages.
|
||||
*/
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the messages corresponding to a user and presents them in a table.
|
||||
*/
|
||||
const UserMessagesCell = ({ path }: UserMessagesCellProps) => {
|
||||
const url = path || "/api/messages/user";
|
||||
const { data: messages, isLoading } = useSWR(url, get, {
|
||||
refreshInterval: 2000,
|
||||
});
|
||||
// TODO(#651): This box coloring and styling is used in multiple places. We
|
||||
// should factor it into a common ui component.
|
||||
const boxBgColor = useColorModeValue("white", "gray.700");
|
||||
const boxAccentColor = useColorModeValue("gray.200", "gray.900");
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor={boxBgColor}
|
||||
boxShadow="base"
|
||||
dropShadow={boxAccentColor}
|
||||
borderRadius="xl"
|
||||
className="p-6 shadow-sm"
|
||||
>
|
||||
{isLoading ? <CircularProgress isIndeterminate /> : <MessageTable messages={messages} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { UserMessagesCell };
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./UserMessagesCell";
|
||||
@@ -8,18 +8,7 @@ import type { FetchUsersResponse, User } from "src/types/Users";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable/DataTable";
|
||||
|
||||
interface Pagination {
|
||||
/**
|
||||
* The user's `display_name` used for pagination.
|
||||
*/
|
||||
cursor: string;
|
||||
|
||||
/**
|
||||
* The pagination direction.
|
||||
*/
|
||||
direction: "forward" | "back";
|
||||
}
|
||||
import { useCursorPagination } from "./DataTable/useCursorPagination";
|
||||
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
@@ -56,12 +45,13 @@ const columns: DataTableColumnDef<User>[] = [
|
||||
];
|
||||
|
||||
export const UserTable = memo(function UserTable() {
|
||||
const [pagination, setPagination] = useState<Pagination>({ cursor: "", direction: "forward" });
|
||||
const { pagination, resetCursor, toNextPage, toPreviousPage } = useCursorPagination();
|
||||
const [filterValues, setFilterValues] = useState<FilterItem[]>([]);
|
||||
const handleFilterValuesChange = (values: FilterItem[]) => {
|
||||
setFilterValues(values);
|
||||
setPagination((old) => ({ ...old, cursor: "" }));
|
||||
resetCursor();
|
||||
};
|
||||
|
||||
// Fetch and save the users.
|
||||
// This follows useSWR's recommendation for simple pagination:
|
||||
// https://swr.vercel.app/docs/pagination#when-to-use-useswr
|
||||
@@ -74,20 +64,6 @@ export const UserTable = memo(function UserTable() {
|
||||
}
|
||||
);
|
||||
|
||||
const toPreviousPage = () => {
|
||||
setPagination({
|
||||
cursor: data?.prev || "",
|
||||
direction: "back",
|
||||
});
|
||||
};
|
||||
|
||||
const toNextPage = () => {
|
||||
setPagination({
|
||||
cursor: data?.next || "",
|
||||
direction: "forward",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
@@ -95,8 +71,8 @@ export const UserTable = memo(function UserTable() {
|
||||
data={data?.items || []}
|
||||
columns={columns}
|
||||
caption="Users"
|
||||
onNextClick={toNextPage}
|
||||
onPreviousClick={toPreviousPage}
|
||||
onNextClick={() => toNextPage(data)}
|
||||
onPreviousClick={() => toPreviousPage(data)}
|
||||
disableNext={!data?.next}
|
||||
disablePrevious={!data?.prev}
|
||||
filterValues={filterValues}
|
||||
|
||||
Reference in New Issue
Block a user