feat: category export and API cleanup.
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user