383 lines
10 KiB
TypeScript
383 lines
10 KiB
TypeScript
import { useState, useMemo, memo } from "react";
|
|
import {
|
|
Text,
|
|
TextInput,
|
|
Stack,
|
|
Group,
|
|
Box,
|
|
Container,
|
|
Divider,
|
|
UnstyledButton,
|
|
Select,
|
|
Pagination,
|
|
Code,
|
|
Alert,
|
|
} from "@mantine/core";
|
|
import {
|
|
MagnifyingGlassIcon,
|
|
CaretUpIcon,
|
|
CaretDownIcon,
|
|
CheckIcon,
|
|
XIcon,
|
|
ChecksIcon,
|
|
} from "@phosphor-icons/react";
|
|
import { Activity, ActivitySearchParams } from "../types";
|
|
import { useActivities } from "../queries";
|
|
import Sheet from "@/components/sheet/sheet";
|
|
import { useSheet } from "@/hooks/use-sheet";
|
|
|
|
interface ActivityListItemProps {
|
|
activity: Activity;
|
|
onClick: () => void;
|
|
}
|
|
|
|
const ActivityListItem = memo(({ activity, onClick }: ActivityListItemProps) => {
|
|
const playerName = typeof activity.player === "object" && activity.player
|
|
? `${activity.player.first_name} ${activity.player.last_name}`
|
|
: "System";
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString();
|
|
};
|
|
|
|
return (
|
|
<UnstyledButton
|
|
w="100%"
|
|
p="md"
|
|
onClick={onClick}
|
|
style={{
|
|
borderRadius: 0,
|
|
transition: "background-color 0.15s ease",
|
|
}}
|
|
styles={{
|
|
root: {
|
|
'&:hover': {
|
|
backgroundColor: 'var(--mantine-color-gray-0)',
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<Group justify="space-between" align="flex-start" w="100%">
|
|
<Stack gap={4} flex={1}>
|
|
<Group gap="xs">
|
|
<Text size="sm" fw={600}>
|
|
{activity.name}
|
|
</Text>
|
|
{activity.success ? (
|
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
|
) : (
|
|
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
|
)}
|
|
</Group>
|
|
<Group gap="md">
|
|
<Text size="xs" c="dimmed">
|
|
{playerName}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{activity.duration}ms
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{formatDate(activity.created)}
|
|
</Text>
|
|
</Group>
|
|
{activity.error && (
|
|
<Text size="xs" c="red" lineClamp={1}>
|
|
{activity.error}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</Group>
|
|
</UnstyledButton>
|
|
);
|
|
});
|
|
|
|
interface ActivityDetailsSheetProps {
|
|
activity: Activity | null;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const ActivityDetailsSheet = memo(({ activity, isOpen, onClose }: ActivityDetailsSheetProps) => {
|
|
if (!activity) return null;
|
|
|
|
const playerName = typeof activity.player === "object" && activity.player
|
|
? `${activity.player.first_name} ${activity.player.last_name}`
|
|
: "System";
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString();
|
|
};
|
|
|
|
return (
|
|
<Sheet title="Activity Details" opened={isOpen} onChange={onClose}>
|
|
<Stack gap="md" p="md">
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Function Name
|
|
</Text>
|
|
<Text size="sm">{activity.name}</Text>
|
|
</Stack>
|
|
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Status
|
|
</Text>
|
|
<Group gap="xs">
|
|
{activity.success ? (
|
|
<>
|
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
|
<Text size="sm" c="green">
|
|
Success
|
|
</Text>
|
|
</>
|
|
) : (
|
|
<>
|
|
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
|
<Text size="sm" c="red">
|
|
Failed
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Group>
|
|
</Stack>
|
|
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Player
|
|
</Text>
|
|
<Text size="sm">{playerName}</Text>
|
|
</Stack>
|
|
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Duration
|
|
</Text>
|
|
<Text size="sm">{activity.duration}ms</Text>
|
|
</Stack>
|
|
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Created
|
|
</Text>
|
|
<Text size="sm">{formatDate(activity.created)}</Text>
|
|
</Stack>
|
|
|
|
{activity.user_agent && (
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
User Agent
|
|
</Text>
|
|
<Text size="xs" style={{ wordBreak: "break-word" }}>
|
|
{activity.user_agent}
|
|
</Text>
|
|
</Stack>
|
|
)}
|
|
|
|
{activity.error && (
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Error Message
|
|
</Text>
|
|
<Alert color="red" variant="light">
|
|
<Text size="sm" style={{ wordBreak: "break-word" }}>
|
|
{activity.error}
|
|
</Text>
|
|
</Alert>
|
|
</Stack>
|
|
)}
|
|
|
|
{activity.arguments && (
|
|
<Stack gap="xs">
|
|
<Text size="xs" fw={700} c="dimmed">
|
|
Arguments
|
|
</Text>
|
|
<Code block style={{ fontSize: "11px" }}>
|
|
{JSON.stringify(activity.arguments, null, 2)}
|
|
</Code>
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
</Sheet>
|
|
);
|
|
});
|
|
|
|
const ActivitiesResults = ({ searchParams, page, setPage, onActivityClick }: any) => {
|
|
const { data: result } = useActivities(searchParams);
|
|
return (
|
|
<>
|
|
<Stack gap={0}>
|
|
{result.items.map((activity: Activity, index: number) => (
|
|
<Box key={activity.id}>
|
|
<ActivityListItem
|
|
activity={activity}
|
|
onClick={() => onActivityClick(activity)}
|
|
/>
|
|
{index < result.items.length - 1 && <Divider />}
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
|
|
{result.items.length === 0 && (
|
|
<Text ta="center" c="dimmed" py="xl">
|
|
No activities found
|
|
</Text>
|
|
)}
|
|
|
|
{result.totalPages > 1 && (
|
|
<Group justify="center" py="md">
|
|
<Pagination
|
|
total={result.totalPages}
|
|
value={page}
|
|
onChange={setPage}
|
|
size="sm"
|
|
/>
|
|
</Group>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const ActivitiesTable = () => {
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState("");
|
|
const [successFilter, setSuccessFilter] = useState<string | null>(null);
|
|
const [sortBy, setSortBy] = useState("-created");
|
|
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
|
|
|
|
const {
|
|
isOpen: detailsOpened,
|
|
open: openDetails,
|
|
close: closeDetails,
|
|
} = useSheet();
|
|
|
|
const searchParams: ActivitySearchParams = useMemo(
|
|
() => ({
|
|
page,
|
|
perPage: 100,
|
|
name: search || undefined,
|
|
success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined,
|
|
sortBy,
|
|
}),
|
|
[page, search, successFilter, sortBy]
|
|
);
|
|
|
|
const { data: result } = useActivities(searchParams);
|
|
|
|
const handleSort = (field: string) => {
|
|
setSortBy((prev) => {
|
|
if (prev === field) return `-${field}`;
|
|
if (prev === `-${field}`) return field;
|
|
return `-${field}`;
|
|
});
|
|
};
|
|
|
|
const getSortIcon = (field: string) => {
|
|
if (sortBy === field) return <CaretUpIcon size={14} />;
|
|
if (sortBy === `-${field}`) return <CaretDownIcon size={14} />;
|
|
return null;
|
|
};
|
|
|
|
const handleActivityClick = (activity: Activity) => {
|
|
setSelectedActivity(activity);
|
|
openDetails();
|
|
};
|
|
|
|
const handleCloseDetails = () => {
|
|
setSelectedActivity(null);
|
|
closeDetails();
|
|
};
|
|
|
|
return (
|
|
<Container size="100%" px={0}>
|
|
<Stack gap="xs">
|
|
<Stack gap="xs" px="md">
|
|
<TextInput
|
|
placeholder="serverFn name"
|
|
value={search}
|
|
onChange={(e) => {
|
|
setSearch(e.currentTarget.value);
|
|
setPage(1);
|
|
}}
|
|
leftSection={<MagnifyingGlassIcon size={16} />}
|
|
size="md"
|
|
/>
|
|
|
|
<Group>
|
|
<Select
|
|
placeholder="Status"
|
|
value={successFilter}
|
|
onChange={(value) => {
|
|
setSuccessFilter(value);
|
|
setPage(1);
|
|
}}
|
|
data={[
|
|
{ value: "all", label: "All" },
|
|
{ value: "success", label: "Success" },
|
|
{ value: "failure", label: "Failure" },
|
|
]}
|
|
clearable
|
|
size="sm"
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</Group>
|
|
</Stack>
|
|
|
|
<Group px="md" justify="space-between" align="center">
|
|
<Text size="10px" lh={0} c="dimmed">
|
|
{result.totalItems} total activities
|
|
</Text>
|
|
<Group gap="xs">
|
|
<Text size="xs" c="dimmed">
|
|
Sort:
|
|
</Text>
|
|
<UnstyledButton
|
|
onClick={() => handleSort("created")}
|
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
|
>
|
|
<Text
|
|
size="xs"
|
|
fw={sortBy.includes("created") ? 600 : 400}
|
|
c={sortBy.includes("created") ? "dark" : "dimmed"}
|
|
>
|
|
Date
|
|
</Text>
|
|
{getSortIcon("created")}
|
|
</UnstyledButton>
|
|
<Text size="xs" c="dimmed">
|
|
•
|
|
</Text>
|
|
<UnstyledButton
|
|
onClick={() => handleSort("duration")}
|
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
|
>
|
|
<Text
|
|
size="xs"
|
|
fw={sortBy.includes("duration") ? 600 : 400}
|
|
c={sortBy.includes("duration") ? "dark" : "dimmed"}
|
|
>
|
|
Duration
|
|
</Text>
|
|
{getSortIcon("duration")}
|
|
</UnstyledButton>
|
|
</Group>
|
|
</Group>
|
|
|
|
<ActivitiesResults
|
|
searchParams={searchParams}
|
|
page={page}
|
|
setPage={setPage}
|
|
onActivityClick={handleActivityClick}
|
|
/>
|
|
</Stack>
|
|
|
|
<ActivityDetailsSheet
|
|
activity={selectedActivity}
|
|
isOpen={detailsOpened}
|
|
onClose={handleCloseDetails}
|
|
/>
|
|
</Container>
|
|
);
|
|
};
|