packages/devtools-ui/src/pages/monitor.tsx
import React from "react";
import clsx from "clsx";
import {
DevToolsContext,
DevtoolsEvent,
receive,
hooksByScope,
RefineHook,
Scopes,
scopes,
} from "@refinedev/devtools-shared";
import {
Cell,
ColumnDef,
SortingState,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import dayjs from "dayjs";
import { ResizablePane } from "src/components/resizable-pane";
import type { Activity } from "src/interfaces/activity";
import { Status } from "src/components/status";
import { MonitorDetails } from "src/components/monitor-details";
import { TraceList } from "src/components/trace-list";
import { MonitorTable } from "src/components/monitor-table";
import { Filters, MonitorFilters } from "src/components/monitor-filters";
import { useSearchParams } from "react-router-dom";
import { getResourceValue } from "src/utils/get-resource-value";
import { ResourceValue } from "src/components/resource-value";
import { useLocalStorage } from "src/hooks/use-local-storage";
export const Monitor = () => {
const { ws } = React.useContext(DevToolsContext);
const [activities, setActivities] = React.useState<Activity[]>([]);
const [searchParams] = useSearchParams();
const highlightParam = searchParams.get("highlight");
const fetchActivities = React.useCallback(() => {
fetch("/api/activities")
.then((res) => res.json())
.then((data) => {
setActivities(data.data);
});
}, []);
React.useEffect(() => {
fetchActivities();
}, [fetchActivities]);
React.useEffect(() => {
if (!ws) return () => 0;
const unsub = receive(
ws,
DevtoolsEvent.DEVTOOLS_ACTIVITY_UPDATE,
(payload) => {
payload.updatedActivities.forEach((activity) => {
setActivities((activities) => {
const index = activities.findIndex(
(a) => a.identifier === activity.identifier,
);
if (index === -1) {
return [...activities, activity];
}
return [
...activities.slice(0, index),
activity,
...activities.slice(index + 1),
];
});
});
},
);
return unsub;
}, [ws]);
const columns = React.useMemo(
() =>
[
{
header: "Hook",
minSize: 90,
accessorFn: (activity) => activity.hookName,
},
{
header: "Resource",
minSize: 100,
accessorFn: getResourceValue,
cell: (cell: Cell<Activity, string>) => {
return (
<ResourceValue
resource={cell.getValue() as string}
scope={scopes[cell.row.original.hookName as RefineHook]}
/>
);
},
},
{
header: "Status",
minSize: 90,
accessorFn: (activity) => activity.status,
cell: (cell: Cell<Activity, Activity["status"]>) => (
<Status activity={cell.row.original} />
),
},
{
header: "Trace",
minSize: 280,
accessorFn: (activity) =>
[...(activity.trace?.map((t) => t.function) ?? [])].reverse(),
cell: (cell: Cell<Activity, string[] | undefined>) => {
return <TraceList trace={cell.row.original.trace} />;
},
},
{
header: "Created At",
minSize: 100,
accessorKey: "createdAt",
cell: (cell: Cell<Activity, Activity["createdAt"]>) => {
return dayjs(cell.getValue()).format("HH:mm:ss");
},
},
{
header: "Updated At",
minSize: 100,
accessorKey: "updatedAt",
cell: (cell: Cell<Activity, Activity["updatedAt"]>) => {
return dayjs(cell.getValue()).format("HH:mm:ss");
},
},
] as ColumnDef<Activity>[],
[],
);
const [filters, setFilters] = useLocalStorage<Filters>({
name: "monitor-filters",
defaultValue: {
hook: [],
parent: [],
resource: undefined,
scope: ["data"],
status: [],
},
});
React.useEffect(() => {
if (highlightParam) {
setFilters({
hook: [],
resource: undefined,
scope: [],
status: [],
parent: [highlightParam],
});
}
}, [highlightParam]);
const data = React.useMemo(() => {
let filtered = [...activities];
if (filters.scope && filters.scope.length > 0) {
const allowedHooks: RefineHook[] = [];
filters.scope.forEach((scope) => {
allowedHooks.push(...hooksByScope[scope as Scopes]);
});
filtered = filtered.filter((activity) =>
allowedHooks.includes(activity.hookName as RefineHook),
);
}
if (filters.hook && filters.hook.length > 0) {
filtered = filtered.filter((activity) =>
filters.hook.includes(activity.hookName),
);
}
if (filters.parent && filters.parent.length > 0) {
filtered = filtered.filter((activity) => {
return activity.trace?.some((trace) => {
if (!trace.function) return false;
return filters.parent.includes(trace.function);
});
});
}
if (filters.resource) {
filtered = filtered.filter((activity) => {
const resource = getResourceValue(activity);
resource.includes(filters.resource as string);
});
}
if (filters.status && filters.status.length > 0) {
filtered = filtered.filter((activity) =>
filters.status.includes(activity.status as any),
);
}
return filtered;
}, [activities, filters]);
const [sorting, setSorting] = React.useState<SortingState>([
{
id: "createdAt",
desc: true,
},
]);
const tableInstance = useReactTable({
columns,
data,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
const [selectedActivity, setSelectedActivity] = React.useState<string | null>(
null,
);
const selectedActivityRecord = React.useMemo(
() => activities.find((el) => el.identifier === selectedActivity),
[activities, selectedActivity],
);
return (
<div className={clsx("flex-1", "re-h-full", "re-overflow-clip")}>
<div
className={clsx(
"re-flex",
"re-flex-col",
"re-gap-4",
"re-h-full",
"re-pb-4",
"md:re-pb-6",
"lg:re-pb-8",
)}
>
<div
className={clsx("re-flex", "re-items-center", "re-justify-between")}
>
<div
className={clsx("re-flex", "re-items-center", "re-justify-start")}
>
<div
className={clsx(
"re-text-gray-0",
"re-font-semibold",
"re-text-sm",
"re-leading-6",
)}
>
Monitor
</div>
<MonitorFilters
filters={filters}
activities={activities}
onSubmit={(f) => setFilters(f)}
/>
</div>
<div>
<div className={clsx("re-text-xs", "re-text-gray-300")}>
Count: {activities.length}
</div>
</div>
</div>
<div className={clsx("re-flex-1", "re-overflow-hidden")}>
<ResizablePane
left={
<MonitorTable
table={tableInstance}
columns={columns}
onSelect={setSelectedActivity}
selected={selectedActivity}
/>
}
right={<MonitorDetails activity={selectedActivityRecord} />}
className={clsx("re-h-full")}
leftClassName={clsx(
"re-rounded-lg",
"re-border",
"re-border-gray-700",
"re-overflow-auto",
"re-flex",
"re-flex-col",
)}
rightClassName={clsx(
"re-rounded-lg",
"re-border",
"re-border-gray-700",
"re-overflow-auto",
)}
/>
</div>
</div>
</div>
);
};