mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-08-15 12:05:19 +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:
Generated
+18
@@ -27,6 +27,7 @@
|
|||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"boolean": "^3.2.0",
|
"boolean": "^3.2.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"eslint": "8.29.0",
|
"eslint": "8.29.0",
|
||||||
"eslint-config-next": "13.0.6",
|
"eslint-config-next": "13.0.6",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
@@ -17710,6 +17711,18 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/date-fns"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.7",
|
"version": "1.11.7",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
|
||||||
@@ -51947,6 +51960,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||||
|
},
|
||||||
"dayjs": {
|
"dayjs": {
|
||||||
"version": "1.11.7",
|
"version": "1.11.7",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"boolean": "^3.2.0",
|
"boolean": "^3.2.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
"eslint": "8.29.0",
|
"eslint": "8.29.0",
|
||||||
"eslint-config-next": "13.0.6",
|
"eslint-config-next": "13.0.6",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
|
|||||||
@@ -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 React from "react";
|
||||||
import { Message } from "src/types/Conversation";
|
import { Message } from "src/types/Conversation";
|
||||||
|
|
||||||
import { MessageTable } from "./MessageTable";
|
import { MessageConversation } from "./MessageConversation";
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default {
|
export default {
|
||||||
title: "Messages/MessageTable",
|
title: "Messages/MessageTable",
|
||||||
component: MessageTable,
|
component: MessageConversation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Template = ({
|
const Template = ({
|
||||||
@@ -21,7 +21,7 @@ const Template = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<MessageTable messages={messages} enableLink={enableLink} highlightLastMessage={highlightLastMessage} />;
|
<MessageConversation messages={messages} enableLink={enableLink} highlightLastMessage={highlightLastMessage} />;
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+2
-2
@@ -2,13 +2,13 @@ import { Stack } from "@chakra-ui/react";
|
|||||||
import { MessageTableEntry } from "src/components/Messages/MessageTableEntry";
|
import { MessageTableEntry } from "src/components/Messages/MessageTableEntry";
|
||||||
import { Message } from "src/types/Conversation";
|
import { Message } from "src/types/Conversation";
|
||||||
|
|
||||||
interface MessageTableProps {
|
interface MessageConversationProps {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
enableLink?: boolean;
|
enableLink?: boolean;
|
||||||
highlightLastMessage?: boolean;
|
highlightLastMessage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageTable({ messages, enableLink, highlightLastMessage }: MessageTableProps) {
|
export function MessageConversation({ messages, enableLink, highlightLastMessage }: MessageConversationProps) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing="4">
|
<Stack spacing="4">
|
||||||
{messages.map((message, idx) => (
|
{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 { useHasRole } from "src/hooks/auth/useHasRole";
|
||||||
import { MessageEmoji } from "src/types/Conversation";
|
import { MessageEmoji } from "src/types/Conversation";
|
||||||
import { emojiIcons } from "src/types/Emoji";
|
import { emojiIcons } from "src/types/Emoji";
|
||||||
@@ -6,10 +6,11 @@ import { emojiIcons } from "src/types/Emoji";
|
|||||||
interface MessageEmojiButtonProps {
|
interface MessageEmojiButtonProps {
|
||||||
emoji: MessageEmoji;
|
emoji: MessageEmoji;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onClick: () => void;
|
onClick?: () => void;
|
||||||
userIsAuthor: boolean;
|
userIsAuthor: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
userReacted: boolean;
|
userReacted: boolean;
|
||||||
|
sx?: ButtonProps["sx"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageEmojiButton = ({
|
export const MessageEmojiButton = ({
|
||||||
@@ -19,6 +20,7 @@ export const MessageEmojiButton = ({
|
|||||||
userIsAuthor,
|
userIsAuthor,
|
||||||
disabled,
|
disabled,
|
||||||
userReacted,
|
userReacted,
|
||||||
|
sx,
|
||||||
}: MessageEmojiButtonProps) => {
|
}: MessageEmojiButtonProps) => {
|
||||||
const EmojiIcon = emojiIcons.get(emoji.name);
|
const EmojiIcon = emojiIcons.get(emoji.name);
|
||||||
const isAdmin = useHasRole("admin");
|
const isAdmin = useHasRole("admin");
|
||||||
@@ -42,8 +44,9 @@ export const MessageEmojiButton = ({
|
|||||||
":hover": {
|
":hover": {
|
||||||
backgroundColor: isDisabled ? "transparent" : undefined,
|
backgroundColor: isDisabled ? "transparent" : undefined,
|
||||||
},
|
},
|
||||||
|
...sx,
|
||||||
}}
|
}}
|
||||||
color={isDisabled ? "gray.500" : undefined}
|
color={isDisabled ? (checked ? "gray.700" : "gray.500") : undefined}
|
||||||
>
|
>
|
||||||
<EmojiIcon style={{ height: "1em" }} />
|
<EmojiIcon style={{ height: "1em" }} />
|
||||||
{showCount && <span style={{ marginInlineEnd: "0.25em" }}>{emoji.count}</span>}
|
{showCount && <span style={{ marginInlineEnd: "0.25em" }}>{emoji.count}</span>}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
import { Box, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useState } from "react";
|
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 { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
|
||||||
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||||
@@ -44,7 +44,7 @@ export const CreateTask = ({
|
|||||||
<TaskHeader taskType={taskType} />
|
<TaskHeader taskType={taskType} />
|
||||||
{task.type !== TaskType.initial_prompt && (
|
{task.type !== TaskType.initial_prompt && (
|
||||||
<Box mt="4" borderRadius="lg" bg={cardColor} className="p-3 sm:p-6">
|
<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>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||||
import { useEffect, useState } from "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 { Sortable } from "src/components/Sortable/Sortable";
|
||||||
import { SurveyCard } from "src/components/Survey/SurveyCard";
|
import { SurveyCard } from "src/components/Survey/SurveyCard";
|
||||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||||
@@ -47,7 +47,7 @@ export const EvaluateTask = ({
|
|||||||
<SurveyCard>
|
<SurveyCard>
|
||||||
<TaskHeader taskType={taskType} />
|
<TaskHeader taskType={taskType} />
|
||||||
<Box mt="4" p="6" borderRadius="lg" bg={cardColor}>
|
<Box mt="4" p="6" borderRadius="lg" bg={cardColor}>
|
||||||
<MessageTable messages={messages} highlightLastMessage />
|
<MessageConversation messages={messages} highlightLastMessage />
|
||||||
</Box>
|
</Box>
|
||||||
<Sortable
|
<Sortable
|
||||||
items={task[sortables]}
|
items={task[sortables]}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Box, useBoolean, useColorModeValue } from "@chakra-ui/react";
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { LabelInputGroup } from "src/components/Messages/LabelInputGroup";
|
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 { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
|
||||||
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
import { TaskSurveyProps } from "src/components/Tasks/Task";
|
||||||
import { TaskHeader } from "src/components/Tasks/TaskHeader";
|
import { TaskHeader } from "src/components/Tasks/TaskHeader";
|
||||||
@@ -57,7 +57,7 @@ export const LabelTask = ({
|
|||||||
<>
|
<>
|
||||||
<TaskHeader taskType={taskType} />
|
<TaskHeader taskType={taskType} />
|
||||||
<Box mt="4" p={[4, 6]} borderRadius="lg" bg={cardColor}>
|
<Box mt="4" p={[4, 6]} borderRadius="lg" bg={cardColor}>
|
||||||
<MessageTable messages={task.conversation.messages} highlightLastMessage />
|
<MessageConversation messages={task.conversation.messages} highlightLastMessage />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
<LabelInputGroup
|
<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 useSWR from "swr";
|
||||||
|
|
||||||
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable/DataTable";
|
import { DataTable, DataTableColumnDef, FilterItem } from "./DataTable/DataTable";
|
||||||
|
import { useCursorPagination } from "./DataTable/useCursorPagination";
|
||||||
interface Pagination {
|
|
||||||
/**
|
|
||||||
* The user's `display_name` used for pagination.
|
|
||||||
*/
|
|
||||||
cursor: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pagination direction.
|
|
||||||
*/
|
|
||||||
direction: "forward" | "back";
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
|
|
||||||
@@ -56,12 +45,13 @@ const columns: DataTableColumnDef<User>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const UserTable = memo(function UserTable() {
|
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 [filterValues, setFilterValues] = useState<FilterItem[]>([]);
|
||||||
const handleFilterValuesChange = (values: FilterItem[]) => {
|
const handleFilterValuesChange = (values: FilterItem[]) => {
|
||||||
setFilterValues(values);
|
setFilterValues(values);
|
||||||
setPagination((old) => ({ ...old, cursor: "" }));
|
resetCursor();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch and save the users.
|
// Fetch and save the users.
|
||||||
// This follows useSWR's recommendation for simple pagination:
|
// This follows useSWR's recommendation for simple pagination:
|
||||||
// https://swr.vercel.app/docs/pagination#when-to-use-useswr
|
// 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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -95,8 +71,8 @@ export const UserTable = memo(function UserTable() {
|
|||||||
data={data?.items || []}
|
data={data?.items || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
caption="Users"
|
caption="Users"
|
||||||
onNextClick={toNextPage}
|
onNextClick={() => toNextPage(data)}
|
||||||
onPreviousClick={toPreviousPage}
|
onPreviousClick={() => toPreviousPage(data)}
|
||||||
disableNext={!data?.next}
|
disableNext={!data?.next}
|
||||||
disablePrevious={!data?.prev}
|
disablePrevious={!data?.prev}
|
||||||
filterValues={filterValues}
|
filterValues={filterValues}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { EmojiOp, Message } from "src/types/Conversation";
|
import type { EmojiOp, FetchUserMessagesCursorResponse, Message } from "src/types/Conversation";
|
||||||
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
import { LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
|
||||||
import type { AvailableTasks } from "src/types/Task";
|
import type { AvailableTasks } from "src/types/Task";
|
||||||
import { FetchTrollBoardResponse, TrollboardTimeFrame } from "src/types/Trollboard";
|
import { FetchTrollBoardResponse, TrollboardTimeFrame } from "src/types/Trollboard";
|
||||||
@@ -264,11 +264,33 @@ export class OasstApiClient {
|
|||||||
return this.get<Message[]>(`/api/v1/users/${user_id}/messages`);
|
return this.get<Message[]>(`/api/v1/users/${user_id}/messages`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetch_user_messages_cursor(
|
||||||
|
user_id: string,
|
||||||
|
{
|
||||||
|
direction,
|
||||||
|
cursor,
|
||||||
|
...rest
|
||||||
|
}: { include_deleted?: boolean; max_count?: number; cursor?: string; direction: "forward" | "back"; desc?: boolean }
|
||||||
|
) {
|
||||||
|
return this.get<FetchUserMessagesCursorResponse>(`/api/v1/users/${user_id}/messages/cursor`, {
|
||||||
|
...rest,
|
||||||
|
after: direction === "forward" ? cursor : undefined,
|
||||||
|
before: direction === "back" ? cursor : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the backend's knowledge about the `user_id`.
|
* Updates the backend's knowledge about the `user_id`.
|
||||||
*/
|
*/
|
||||||
async set_user_status(user_id: string, is_enabled: boolean, notes: string): Promise<void> {
|
async set_user_status(
|
||||||
await this.put(`/api/v1/users/${user_id}?enabled=${is_enabled}¬es=${notes}`);
|
user_id: string,
|
||||||
|
is_enabled: boolean,
|
||||||
|
notes: string,
|
||||||
|
show_on_leaderboard: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
await this.put(
|
||||||
|
`/api/v1/users/${user_id}?enabled=${is_enabled}¬es=${notes}&show_on_leaderboard=${show_on_leaderboard}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const ROUTES = {
|
||||||
|
ADMIN_MESSAGE_DETAIL: (id: string) => `/admin/messages/${id}`,
|
||||||
|
};
|
||||||
@@ -1,19 +1,39 @@
|
|||||||
import { Button, Card, CardBody, Container, FormControl, FormLabel, Input, Stack, useToast } from "@chakra-ui/react";
|
import {
|
||||||
import { InferGetServerSidePropsType } from "next";
|
Accordion,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Checkbox,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
useToast,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { AdminArea } from "src/components/AdminArea";
|
||||||
|
import { useCursorPagination } from "src/components/DataTable/useCursorPagination";
|
||||||
|
import { JsonCard } from "src/components/JsonCard";
|
||||||
import { getAdminLayout } from "src/components/Layout";
|
import { getAdminLayout } from "src/components/Layout";
|
||||||
|
import { AdminMessageTable } from "src/components/Messages/AdminMessageTable";
|
||||||
import { Role, RoleSelect } from "src/components/RoleSelect";
|
import { Role, RoleSelect } from "src/components/RoleSelect";
|
||||||
import { UserMessagesCell } from "src/components/UserMessagesCell";
|
import { get, post } from "src/lib/api";
|
||||||
import { post } from "src/lib/api";
|
|
||||||
import { userlessApiClient } from "src/lib/oasst_client_factory";
|
import { userlessApiClient } from "src/lib/oasst_client_factory";
|
||||||
import prisma from "src/lib/prismadb";
|
import prisma from "src/lib/prismadb";
|
||||||
|
import { FetchUserMessagesCursorResponse } from "src/types/Conversation";
|
||||||
|
import { User } from "src/types/Users";
|
||||||
|
import useSWRImmutable from "swr/immutable";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
|
|
||||||
interface UserForm {
|
interface UserForm {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,33 +41,17 @@ interface UserForm {
|
|||||||
display_name: string;
|
display_name: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
notes: string;
|
notes: string;
|
||||||
|
show_on_leaderboard: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const router = useRouter();
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
|
|
||||||
// Check when the user session is loaded and re-route if the user is not an
|
|
||||||
// admin. This follows the suggestion by NextJS for handling private pages:
|
|
||||||
// https://nextjs.org/docs/api-reference/next/router#usage
|
|
||||||
//
|
|
||||||
// All admin pages should use the same check and routing steps.
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === "loading") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (session?.user?.role === "admin") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push("/");
|
|
||||||
}, [router, session, status]);
|
|
||||||
|
|
||||||
// Trigger to let us update the user's role. Triggers a toast when complete.
|
// Trigger to let us update the user's role. Triggers a toast when complete.
|
||||||
const { trigger } = useSWRMutation("/api/admin/update_user", post, {
|
const { trigger } = useSWRMutation("/api/admin/update_user", post, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
title: "User Role Updated",
|
title: "Updated user",
|
||||||
status: "success",
|
status: "success",
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -76,8 +80,8 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
|
|||||||
content="Conversational AI for everyone. An open source project to create a chat enabled GPT LLM run by LAION and contributors around the world."
|
content="Conversational AI for everyone. An open source project to create a chat enabled GPT LLM run by LAION and contributors around the world."
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<Stack gap="4">
|
<AdminArea>
|
||||||
<Container>
|
<Stack gap="4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<form onSubmit={handleSubmit((data) => trigger(data))}>
|
<form onSubmit={handleSubmit((data) => trigger(data))}>
|
||||||
@@ -88,32 +92,97 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
|
|||||||
<FormLabel>Display Name</FormLabel>
|
<FormLabel>Display Name</FormLabel>
|
||||||
<Input {...register("display_name")} isDisabled />
|
<Input {...register("display_name")} isDisabled />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl mt="2">
|
||||||
<FormLabel>Role</FormLabel>
|
<FormLabel>Role</FormLabel>
|
||||||
<RoleSelect {...register("role")}></RoleSelect>
|
<RoleSelect {...register("role")}></RoleSelect>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl mt="2">
|
||||||
<FormLabel>Notes</FormLabel>
|
<FormLabel>Notes</FormLabel>
|
||||||
<Input {...register("notes")} />
|
<Input {...register("notes")} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl mt="2">
|
||||||
|
<FormLabel>Show on leaderboard</FormLabel>
|
||||||
|
<Checkbox {...register("show_on_leaderboard")}></Checkbox>
|
||||||
|
</FormControl>
|
||||||
<Button mt={4} type="submit">
|
<Button mt={4} type="submit">
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<Accordion allowToggle mt="4">
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
Raw JSON
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<JsonCard>{user}</JsonCard>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</Container>
|
<Card>
|
||||||
<UserMessagesCell path={`/api/admin/user_messages?user=${user.user_id}`} />
|
<CardHeader pb="0" fontWeight="medium" fontSize="xl">
|
||||||
</Stack>
|
{`User's messages`}
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<UserMessageTable id={user.user_id}></UserMessageTable>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</AdminArea>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserMessageTable = ({ id }: { id: User["id"] }) => {
|
||||||
|
const { pagination, toNextPage, toPreviousPage } = useCursorPagination();
|
||||||
|
const { data, error, isLoading } = useSWRImmutable<FetchUserMessagesCursorResponse>(
|
||||||
|
`/api/admin/user_messages?user=${id}&cursor=${encodeURIComponent(pagination.cursor)}&direction=${
|
||||||
|
pagination.direction
|
||||||
|
}`,
|
||||||
|
get,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading && !data) {
|
||||||
|
return <CircularProgress isIndeterminate />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <>Unable to load messages.</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminMessageTable
|
||||||
|
data={data?.items || []}
|
||||||
|
disableNext={!data?.next}
|
||||||
|
disablePrevious={!data?.prev}
|
||||||
|
onNextClick={() => toNextPage(data)}
|
||||||
|
onPreviousClick={() => toPreviousPage(data)}
|
||||||
|
></AdminMessageTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the user's data on the server side when rendering.
|
* Fetch the user's data on the server side when rendering.
|
||||||
*/
|
*/
|
||||||
export async function getServerSideProps({ query, locale }) {
|
export const getServerSideProps: GetServerSideProps<{ user: User<Role> }, { id: string }> = async ({
|
||||||
const backend_user = await userlessApiClient.fetch_user(query.id);
|
params,
|
||||||
|
locale = "en",
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const backend_user = await userlessApiClient.fetch_user(params!.id as string);
|
||||||
|
|
||||||
|
if (!backend_user) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
const local_user = await prisma.user.findUnique({
|
const local_user = await prisma.user.findUnique({
|
||||||
where: { id: backend_user.id },
|
where: { id: backend_user.id },
|
||||||
select: {
|
select: {
|
||||||
@@ -130,7 +199,7 @@ export async function getServerSideProps({ query, locale }) {
|
|||||||
...(await serverSideTranslations(locale, ["common"])),
|
...(await serverSideTranslations(locale, ["common"])),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
ManageUser.getLayout = getAdminLayout;
|
ManageUser.getLayout = getAdminLayout;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import prisma from "src/lib/prismadb";
|
|||||||
* Update's the user's data in the database. Accessible only to admins.
|
* Update's the user's data in the database. Accessible only to admins.
|
||||||
*/
|
*/
|
||||||
const handler = withRole("admin", async (req, res, token) => {
|
const handler = withRole("admin", async (req, res, token) => {
|
||||||
const { id, auth_method, user_id, notes, role } = req.body;
|
const { id, auth_method, user_id, notes, role, show_on_leaderboard } = req.body;
|
||||||
|
|
||||||
const oasstApiClient = await createApiClient(token);
|
const oasstApiClient = await createApiClient(token);
|
||||||
// If the user is authorized by the web, update their role.
|
// If the user is authorized by the web, update their role.
|
||||||
@@ -17,7 +17,7 @@ const handler = withRole("admin", async (req, res, token) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Tell the backend the user's enabled or not enabled status.
|
// Tell the backend the user's enabled or not enabled status.
|
||||||
await oasstApiClient.set_user_status(user_id, role !== "banned", notes);
|
await oasstApiClient.set_user_status(user_id, role !== "banned", notes, show_on_leaderboard);
|
||||||
|
|
||||||
res.status(200).json({});
|
res.status(200).json({});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { withRole } from "src/lib/auth";
|
import { withRole } from "src/lib/auth";
|
||||||
import { createApiClient } from "src/lib/oasst_client_factory";
|
import { createApiClient } from "src/lib/oasst_client_factory";
|
||||||
import type { Message } from "src/types/Conversation";
|
|
||||||
|
const LIMIT = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the messages recorded by the backend for a user.
|
* Returns the messages recorded by the backend for a user.
|
||||||
*/
|
*/
|
||||||
const handler = withRole("admin", async (req, res, token) => {
|
const handler = withRole("admin", async (req, res, token) => {
|
||||||
const { user } = req.query;
|
const { cursor, direction, user } = req.query;
|
||||||
|
|
||||||
const oasstApiClient = await createApiClient(token);
|
const oasstApiClient = await createApiClient(token);
|
||||||
const messages: Message[] = await oasstApiClient.fetch_user_messages(user as string);
|
|
||||||
res.status(200).json(messages);
|
const response = await oasstApiClient.fetch_user_messages_cursor(user as string, {
|
||||||
|
include_deleted: true,
|
||||||
|
direction: direction as "back",
|
||||||
|
cursor: cursor as string,
|
||||||
|
max_count: LIMIT,
|
||||||
|
desc: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useCookies } from "react-cookie";
|
import { useCookies } from "react-cookie";
|
||||||
import { getDashboardLayout } from "src/components/Layout";
|
import { getDashboardLayout } from "src/components/Layout";
|
||||||
import { MessageTable } from "src/components/Messages/MessageTable";
|
import { MessageConversation } from "src/components/Messages/MessageConversation";
|
||||||
import { get } from "src/lib/api";
|
import { get } from "src/lib/api";
|
||||||
import useSWRImmutable from "swr/immutable";
|
import useSWRImmutable from "swr/immutable";
|
||||||
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
|
||||||
@@ -38,7 +38,7 @@ const MessagesDashboard = () => {
|
|||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
className="p-3 sm:p-4 shadow-sm"
|
className="p-3 sm:p-4 shadow-sm"
|
||||||
>
|
>
|
||||||
{messages ? <MessageTable enableLink messages={messages} /> : <CircularProgress isIndeterminate />}
|
{messages ? <MessageConversation enableLink messages={messages} /> : <CircularProgress isIndeterminate />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
@@ -52,7 +52,11 @@ const MessagesDashboard = () => {
|
|||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
className="p-6 shadow-sm"
|
className="p-6 shadow-sm"
|
||||||
>
|
>
|
||||||
{userMessages ? <MessageTable enableLink messages={userMessages} /> : <CircularProgress isIndeterminate />}
|
{userMessages ? (
|
||||||
|
<MessageConversation enableLink messages={userMessages} />
|
||||||
|
) : (
|
||||||
|
<CircularProgress isIndeterminate />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { tableAnatomy } from "@chakra-ui/anatomy";
|
||||||
|
import { createMultiStyleConfigHelpers } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tableAnatomy.keys);
|
||||||
|
|
||||||
|
export const tableTheme = defineMultiStyleConfig({
|
||||||
|
variants: {
|
||||||
|
simple: definePartsStyle(({ colorMode }) => {
|
||||||
|
const isLightMode = colorMode === "light";
|
||||||
|
return {
|
||||||
|
td: {
|
||||||
|
borderColor: isLightMode ? "gray.100" : "gray.800",
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
borderColor: isLightMode ? "gray.100" : "gray.800",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { colors } from "./colors";
|
|||||||
import { badgeTheme } from "./components/Badge";
|
import { badgeTheme } from "./components/Badge";
|
||||||
import { cardTheme } from "./components/Card";
|
import { cardTheme } from "./components/Card";
|
||||||
import { containerTheme } from "./components/Container";
|
import { containerTheme } from "./components/Container";
|
||||||
|
import { tableTheme } from "./components/Table";
|
||||||
|
|
||||||
const config: ThemeConfig = {
|
const config: ThemeConfig = {
|
||||||
initialColorMode: "light",
|
initialColorMode: "light",
|
||||||
@@ -16,6 +17,7 @@ const components = {
|
|||||||
Badge: badgeTheme,
|
Badge: badgeTheme,
|
||||||
Container: containerTheme,
|
Container: containerTheme,
|
||||||
Card: cardTheme,
|
Card: cardTheme,
|
||||||
|
Table: tableTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
const breakpoints = {
|
const breakpoints = {
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ export interface Message extends MessageEmojis {
|
|||||||
export interface Conversation {
|
export interface Conversation {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FetchUserMessagesCursorResponse = {
|
||||||
|
next?: string;
|
||||||
|
prev?: string;
|
||||||
|
sort_key: string;
|
||||||
|
items: Message[];
|
||||||
|
order: "asc" | "desc";
|
||||||
|
};
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ export interface BackendUser extends BackendUserCore {
|
|||||||
/**
|
/**
|
||||||
* An expanded User for the web.
|
* An expanded User for the web.
|
||||||
*/
|
*/
|
||||||
export interface User extends BackendUser {
|
export interface User<TRole extends string = string> extends BackendUser {
|
||||||
/**
|
/**
|
||||||
* The user's roles within the webapp.
|
* The user's roles within the webapp.
|
||||||
*/
|
*/
|
||||||
role: string;
|
role: TRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FetchUsersParams = {
|
export type FetchUsersParams = {
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// https://github.com/ts-essentials/ts-essentials/blob/25cae45c162f8784e3cdae8f43783d0c66370a57/lib/types.ts#L437
|
// https://github.com/ts-essentials/ts-essentials/blob/25cae45c162f8784e3cdae8f43783d0c66370a57/lib/types.ts#L437
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type ElementOf<T extends readonly any[]> = T extends readonly (infer ET)[] ? ET : never;
|
export type ElementOf<T extends readonly any[]> = T extends readonly (infer ET)[] ? ET : never;
|
||||||
|
type AnyRecord<T = any> = Record<KeyofBase, T>;
|
||||||
|
type KeyofBase = keyof any;
|
||||||
|
|
||||||
|
export type AnyArray<T = any> = Array<T> | ReadonlyArray<T>;
|
||||||
|
|
||||||
|
export type StrictOmit<T extends AnyRecord, K extends keyof T> = T extends AnyArray ? never : Omit<T, K>;
|
||||||
|
|||||||
Reference in New Issue
Block a user