- sparklines get start/end date and max-value footers - vitals rating shown by value color instead of badges - error rows use plain counts, subtle resolve button, terse empty state - telemetry tab order: usage, players, functions, events, errors; usage is default
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
import { useState } from "react";
|
||
import { Trans, useLingui } from "@lingui/react/macro";
|
||
import {
|
||
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">
|
||
<Text
|
||
size="sm"
|
||
fw={700}
|
||
c={group.resolved ? "dimmed" : "red"}
|
||
style={{ flexShrink: 0, fontVariantNumeric: "tabular-nums" }}
|
||
>
|
||
{group.count.toLocaleString()}×
|
||
</Text>
|
||
<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="subtle"
|
||
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;
|