feat(admin): dashboard home, telemetry section, enrollment ops screen
- mission-control dashboard: now strip, tournament quick actions, usage sparklines, error feed, live activity, tools list - /admin/telemetry with functions/events/errors/players/usage child routes, hand-rolled svg chart primitives, decomposed activities table - error browser with grouping and mark-resolved - consolidated per-tournament enrollment screen; hub simplified with run tournament promoted - /admin/activities redirects to /admin/telemetry/functions
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { useState } from "react";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Code,
|
||||
Collapse,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { resolveClientErrorGroup } from "../../server";
|
||||
import { telemetryKeys } from "../../queries";
|
||||
import type { ClientErrorGroup } from "../../types";
|
||||
import { RelativeTime } from "../shared";
|
||||
|
||||
export const ErrorGroupRow = ({ group }: { group: ClientErrorGroup }) => {
|
||||
const { t } = useLingui();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const resolveMutation = useServerMutation({
|
||||
mutationFn: (variables: { groupHash: string; resolved: boolean }) =>
|
||||
resolveClientErrorGroup({ data: variables }),
|
||||
successMessage: group.resolved ? t`Error group reopened` : t`Error group resolved`,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: telemetryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<UnstyledButton
|
||||
w="100%"
|
||||
p="md"
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
style={{ borderRadius: 0 }}
|
||||
>
|
||||
<Stack gap={4}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Badge color={group.resolved ? "gray" : "red"} variant="light" size="sm">
|
||||
{group.count.toLocaleString()}
|
||||
</Badge>
|
||||
<Text size="sm" fw={600} lineClamp={2} style={{ minWidth: 0 }}>
|
||||
{group.sample.message}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="md">
|
||||
{group.sample.source && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{group.sample.source}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
<RelativeTime date={group.lastSeen} />
|
||||
</Text>
|
||||
{group.resolved && (
|
||||
<Text size="xs" c="dimmed">
|
||||
<Trans>Resolved</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</UnstyledButton>
|
||||
|
||||
<Collapse in={expanded} transitionDuration={200}>
|
||||
<Stack gap="sm" px="md" pb="md">
|
||||
{group.sample.path && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{group.sample.path}
|
||||
</Text>
|
||||
)}
|
||||
{group.sample.stack && (
|
||||
<Code
|
||||
block
|
||||
style={{ fontSize: "11px", maxHeight: 240, overflow: "auto" }}
|
||||
>
|
||||
{group.sample.stack}
|
||||
</Code>
|
||||
)}
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
color={group.resolved ? "yellow" : "green"}
|
||||
loading={resolveMutation.isPending}
|
||||
onClick={() =>
|
||||
resolveMutation.mutate({
|
||||
groupHash: group.group_hash,
|
||||
resolved: !group.resolved,
|
||||
})
|
||||
}
|
||||
>
|
||||
{group.resolved ? <Trans>Reopen</Trans> : <Trans>Mark resolved</Trans>}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorGroupRow;
|
||||
Reference in New Issue
Block a user