Files
flxn-app/src/features/activities/components/activities-table.tsx
T
kyle 5d7747839c refactor(telemetry): text-only list empty states, distinct p50/p95 series
- 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
2026-08-25 22:38:45 -07:00

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>
);
};