pankod/refine

View on GitHub
packages/devtools-ui/src/pages/monitor.tsx

Summary

Maintainability
C
1 day
Test Coverage
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>
  );
};