feat: category export and API cleanup.

This commit is contained in:
mattia
2025-01-06 20:45:51 +01:00
parent 50ecb39cce
commit 99859177b3
16 changed files with 566 additions and 248 deletions

View File

@@ -1,22 +1,39 @@
import { useParams } from "react-router";
import { client } from "./client";
import {
Autocomplete,
Button,
Card,
CardActions,
CardContent,
CircularProgress,
Collapse,
IconButton,
List,
ListItem,
ListItemText,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useMemo } from "react";
import {
Fragment,
useCallback,
useMemo,
useState,
} from "react";
import {
createMRTColumnHelper,
MaterialReactTable,
MRT_ColumnDef,
useMaterialReactTable,
} from "material-react-table";
import { GetSessionResponse } from "./contract";
import { format } from "date-fns";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import { useQueryClient } from "@tanstack/react-query";
type LogEntry = GetSessionResponse["log_entries"][0];
const columnHelper = createMRTColumnHelper<LogEntry>(); // <--- pass your TData type as a generic to createMRTColumnHelper (if using TS)
export function SessionDetails() {
const { sessionId } = useParams<{ sessionId: string }>();
@@ -28,42 +45,49 @@ export function SessionDetails() {
params: { id: sessionId },
});
const columns = useMemo<
MRT_ColumnDef<GetSessionResponse["log_entries"][0]>[]
>(
const columns = useMemo(
() => [
{
accessorKey: "message",
columnHelper.accessor("category", {
header: "Category",
Cell: ({ cell }) => <Typography>{cell.getValue()}</Typography>,
}),
columnHelper.accessor("message", {
header: "Message",
},
{
accessorKey: "timestamp",
Cell: ({ cell }) => <Typography>{cell.getValue()}</Typography>,
}),
columnHelper.accessor("timestamp", {
header: "Timestamp",
Cell: (x) => <span>{format(x.row.original.timestamp, "Pp")}</span>,
},
{
accessorKey: "metadata",
Cell: ({ cell }) => (
<Typography>{format(cell.getValue(), "Pp")}</Typography>
),
}),
columnHelper.accessor("metadata", {
header: "Metadata",
Cell: (x) => {
const metadata = x.row.original.metadata;
Cell: ({ cell }) => {
const metadata = cell.getValue();
return (
<List dense>
{Object.keys(metadata)
.sort()
.map((key) => (
<ListItem key={key}>
<ListItemText>{key}: {metadata[key]}</ListItemText>
<ListItemText>
<Typography>
{key}: {metadata[key]}
</Typography>
</ListItemText>
</ListItem>
))}
</List>
);
},
},
}),
],
[]
);
const table = useMaterialReactTable({
layoutMode: "grid",
columns,
data:
getSessionQuery.status === "success"
@@ -71,6 +95,62 @@ export function SessionDetails() {
: [],
});
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const toggleExportDialog = useCallback(
() => setIsExportDialogOpen((x) => !x),
[]
);
const getCategoriesQuery = client.getCategories.useQuery(
["get-categories", sessionId],
{
query: {
sessionId,
},
},
{
enabled: isExportDialogOpen,
}
);
const [selectedCategory, setSelectedCategory] = useState("");
const handleSelectedCategoryChange = useCallback(
(_: unknown, newValue: string | null) => {
setSelectedCategory(newValue || "");
},
[]
);
const queryClient = useQueryClient();
const onExport = useCallback(() => {
client.getEntries
.fetchQuery(
queryClient,
["get-entries", sessionId, selectedCategory],
{
params: { sessionId },
query: { category: selectedCategory },
},
{
meta: { causeDownload: true },
}
)
.then((data) => {
const json = JSON.stringify(data.body, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute(
"download",
`entries-${sessionId}-${selectedCategory}.json`
);
a.click();
})
.catch(console.error);
}, [queryClient, sessionId, selectedCategory]);
return getSessionQuery.status === "error" ? (
<p>Error</p>
) : (
@@ -93,6 +173,52 @@ export function SessionDetails() {
: getSessionQuery.data.body.version}
</Typography>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="export category" onClick={toggleExportDialog}>
<FileDownloadIcon />
</IconButton>
</CardActions>
<Collapse in={isExportDialogOpen} timeout="auto" unmountOnExit>
<CardContent>
<Stack spacing={2}>
<Typography variant="h6" component="div">
Export
</Typography>
<Autocomplete
options={
getCategoriesQuery.status === "success"
? getCategoriesQuery.data.body
: []
}
value={selectedCategory}
onChange={handleSelectedCategoryChange}
loading={getCategoriesQuery.isLoading}
renderInput={(params) => (
<TextField
{...params}
label="Category"
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<Fragment>
{getCategoriesQuery.isLoading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</Fragment>
),
},
}}
/>
)}
/>
<Button disabled={!selectedCategory} onClick={onExport}>
Export
</Button>
</Stack>
</CardContent>
</Collapse>
</Card>
{getSessionQuery.status === "success" && (
<MaterialReactTable table={table} />

View File

@@ -7,7 +7,7 @@ import {
type MRT_ColumnDef,
type MRT_PaginationState,
} from "material-react-table";
import { GetSessionsResponse } from "../common/contract";
import { GetSessionsResponse } from "./contract";
import { format, formatDuration, interval, intervalToDuration } from "date-fns";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router";