- paginated-list empty prop is a plain string, no icon block - second sparkline series drawn in dimmed color; area disabled on dual charts - duration legend shows latest p50/p95 values
237 lines
7.8 KiB
TypeScript
237 lines
7.8 KiB
TypeScript
import { useState, useMemo } from "react";
|
|
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
|
import { Alert, Code, Container, Group, Stack, Text } from "@mantine/core";
|
|
import { CheckIcon, XIcon } from "@phosphor-icons/react";
|
|
import { Activity, ActivitySearchParams } from "../types";
|
|
import { useActivities } from "../queries";
|
|
import { usePlayers } from "@/features/players/queries";
|
|
import { useSheet } from "@/hooks/use-sheet";
|
|
import {
|
|
DetailSheet,
|
|
PaginatedList,
|
|
SearchFilterBar,
|
|
TelemetryListItem,
|
|
} from "@/features/telemetry/components/shared";
|
|
|
|
const SLOW_THRESHOLD_MS = 1000;
|
|
|
|
export const ActivitiesTable = () => {
|
|
const { t, i18n } = useLingui();
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState("");
|
|
const [successFilter, setSuccessFilter] = useState<string | null>(null);
|
|
const [playerFilter, setPlayerFilter] = useState<string | null>(null);
|
|
const [slowOnly, setSlowOnly] = useState(false);
|
|
const [sortBy, setSortBy] = useState("-created");
|
|
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
|
|
const { isOpen: detailsOpened, open: openDetails, close: closeDetails } = useSheet();
|
|
|
|
const { data: players } = usePlayers();
|
|
|
|
const searchParams: ActivitySearchParams = useMemo(
|
|
() => ({
|
|
page,
|
|
perPage: 100,
|
|
name: search || undefined,
|
|
success: successFilter === "success" ? true : successFilter === "failure" ? false : undefined,
|
|
player: playerFilter || undefined,
|
|
sortBy,
|
|
}),
|
|
[page, search, successFilter, playerFilter, sortBy]
|
|
);
|
|
|
|
const { data: result } = useActivities(searchParams);
|
|
|
|
const items = useMemo(
|
|
() => (slowOnly ? result.items.filter((a) => a.duration > SLOW_THRESHOLD_MS) : result.items),
|
|
[result.items, slowOnly]
|
|
);
|
|
|
|
const playerName = (activity: Activity) =>
|
|
typeof activity.player === "object" && activity.player
|
|
? `${activity.player.first_name} ${activity.player.last_name}`
|
|
: t`System`;
|
|
|
|
const handleSort = (field: string) => {
|
|
setSortBy((prev) => {
|
|
if (prev === field) return `-${field}`;
|
|
if (prev === `-${field}`) return field;
|
|
return `-${field}`;
|
|
});
|
|
};
|
|
|
|
const handleActivityClick = (activity: Activity) => {
|
|
setSelectedActivity(activity);
|
|
openDetails();
|
|
};
|
|
|
|
const handleCloseDetails = () => {
|
|
setSelectedActivity(null);
|
|
closeDetails();
|
|
};
|
|
|
|
return (
|
|
<Container size="100%" px={0}>
|
|
<Stack gap="xs">
|
|
<SearchFilterBar
|
|
searchValue={search}
|
|
onSearchChange={(value) => {
|
|
setSearch(value);
|
|
setPage(1);
|
|
}}
|
|
searchPlaceholder={t`serverFn name`}
|
|
filters={[
|
|
{
|
|
placeholder: t`Status`,
|
|
value: successFilter,
|
|
onChange: (value) => {
|
|
setSuccessFilter(value);
|
|
setPage(1);
|
|
},
|
|
data: [
|
|
{ value: "all", label: t`All` },
|
|
{ value: "success", label: t`Success` },
|
|
{ value: "failure", label: t`Failure` },
|
|
],
|
|
},
|
|
{
|
|
placeholder: t`Player`,
|
|
value: playerFilter,
|
|
onChange: (value) => {
|
|
setPlayerFilter(value);
|
|
setPage(1);
|
|
},
|
|
data: players.map((player) => ({
|
|
value: player.id,
|
|
label: `${player.first_name} ${player.last_name}`,
|
|
})),
|
|
searchable: true,
|
|
},
|
|
]}
|
|
chips={[
|
|
{
|
|
label: t`Slow >1s (this page)`,
|
|
checked: slowOnly,
|
|
onChange: setSlowOnly,
|
|
},
|
|
]}
|
|
sortFields={[
|
|
{ field: "created", label: <Trans>Date</Trans> },
|
|
{ field: "duration", label: <Trans>Duration</Trans> },
|
|
]}
|
|
sortBy={sortBy}
|
|
onSort={handleSort}
|
|
summary={
|
|
<Plural value={result.totalItems} one="# total activity" other="# total activities" />
|
|
}
|
|
/>
|
|
|
|
<PaginatedList
|
|
items={items}
|
|
keyOf={(activity) => activity.id}
|
|
empty={t`No Activities Found`}
|
|
page={page}
|
|
totalPages={result.totalPages}
|
|
onPageChange={setPage}
|
|
renderItem={(activity) => (
|
|
<TelemetryListItem
|
|
title={activity.name}
|
|
icon={
|
|
activity.success ? (
|
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
|
) : (
|
|
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
|
)
|
|
}
|
|
meta={[
|
|
playerName(activity),
|
|
<Trans>{activity.duration}ms</Trans>,
|
|
new Date(activity.created).toLocaleString(i18n.locale),
|
|
]}
|
|
error={activity.error}
|
|
onClick={() => handleActivityClick(activity)}
|
|
/>
|
|
)}
|
|
/>
|
|
</Stack>
|
|
|
|
<DetailSheet
|
|
title={t`Activity Details`}
|
|
opened={detailsOpened}
|
|
onClose={handleCloseDetails}
|
|
fields={
|
|
selectedActivity
|
|
? [
|
|
{ label: <Trans>Function Name</Trans>, value: selectedActivity.name },
|
|
{
|
|
label: <Trans>Status</Trans>,
|
|
value: (
|
|
<Group gap="xs">
|
|
{selectedActivity.success ? (
|
|
<>
|
|
<CheckIcon size={16} color="var(--mantine-color-green-6)" />
|
|
<Text size="sm" c="green">
|
|
<Trans>Success</Trans>
|
|
</Text>
|
|
</>
|
|
) : (
|
|
<>
|
|
<XIcon size={16} color="var(--mantine-color-red-6)" />
|
|
<Text size="sm" c="red">
|
|
<Trans>Failed</Trans>
|
|
</Text>
|
|
</>
|
|
)}
|
|
</Group>
|
|
),
|
|
},
|
|
{ label: <Trans>Player</Trans>, value: playerName(selectedActivity) },
|
|
{
|
|
label: <Trans>Duration</Trans>,
|
|
value: (
|
|
<Text size="sm">
|
|
<Trans>{selectedActivity.duration}ms</Trans>
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
label: <Trans>Created</Trans>,
|
|
value: new Date(selectedActivity.created).toLocaleString(i18n.locale),
|
|
},
|
|
...(selectedActivity.user_agent
|
|
? [{ label: <Trans>User Agent</Trans>, value: selectedActivity.user_agent }]
|
|
: []),
|
|
...(selectedActivity.error
|
|
? [
|
|
{
|
|
label: <Trans>Error Message</Trans>,
|
|
value: (
|
|
<Alert color="red" variant="light">
|
|
<Text size="sm" style={{ wordBreak: "break-word" }}>
|
|
{selectedActivity.error}
|
|
</Text>
|
|
</Alert>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
...(selectedActivity.arguments
|
|
? [
|
|
{
|
|
label: <Trans>Arguments</Trans>,
|
|
value: (
|
|
<Code block style={{ fontSize: "11px" }}>
|
|
{JSON.stringify(selectedActivity.arguments, null, 2)}
|
|
</Code>
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
]
|
|
: []
|
|
}
|
|
/>
|
|
</Container>
|
|
);
|
|
};
|