import { css } from "@emotion/react";
import type {
  ColumnDef,
  OnChangeFn,
  SortingState,
} from "@tanstack/react-table";
import {
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { formatDistance } from "date-fns";
import {
  memo,
  startTransition,
  Suspense,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import {
  fetchQuery,
  graphql,
  useLazyLoadQuery,
  usePaginationFragment,
  useRelayEnvironment,
} from "react-relay";
import { useNavigate } from "react-router";
import type { Subscription } from "relay-runtime";

import type { FlexProps } from "@phoenix/components";
import {
  DebouncedSearch,
  Flex,
  Heading,
  Icon,
  Icons,
  Link,
  Loading,
  Skeleton,
  Text,
  View,
} from "@phoenix/components";
import { CanModify } from "@phoenix/components/auth";
import { EmptyState, EmptyStateGraphic } from "@phoenix/components/core/empty";
import { LoadMoreButton } from "@phoenix/components/core/LoadMoreButton";
import { Truncate } from "@phoenix/components/core/utility/Truncate";
import {
  ConnectedTimeRangeSelector,
  type TimeRangeISOStrings,
  useTimeRange,
  useTimeRangeSearch,
} from "@phoenix/components/datetime";
import { TopNavActions } from "@phoenix/components/nav";
import { GradientCircle } from "@phoenix/components/project/GradientCircle";
import { selectableTableCSS } from "@phoenix/components/table/styles";
import { TableEmptyWrap } from "@phoenix/components/table/TableEmptyWrap";
import { TimestampCell } from "@phoenix/components/table/TimestampCell";
import { LatencyText } from "@phoenix/components/trace/LatencyText";
import {
  useNotifySuccess,
  usePreferencesContext,
  useViewerCanModify,
} from "@phoenix/contexts";
import { useInterval } from "@phoenix/hooks";
import type {
  ProjectsPageProjectMetricsQuery,
  ProjectsPageProjectMetricsQuery$data,
} from "@phoenix/pages/projects/__generated__/ProjectsPageProjectMetricsQuery.graphql";
import { ProjectViewModeToggle } from "@phoenix/pages/projects/ProjectViewModeToggle";
import type { ProjectSortOrder } from "@phoenix/store/preferencesStore";
import { intFormatter } from "@phoenix/utils/numberFormatUtils";

import type {
  ProjectsPageProjectsFragment$data,
  ProjectsPageProjectsFragment$key,
} from "./__generated__/ProjectsPageProjectsFragment.graphql";
import type {
  ProjectSort,
  ProjectsPageProjectsQuery,
} from "./__generated__/ProjectsPageProjectsQuery.graphql";
import type { ProjectsPageQuery } from "./__generated__/ProjectsPageQuery.graphql";
import { NewProjectButton } from "./NewProjectButton";
import { ProjectActionMenu } from "./ProjectActionMenu";

const PAGE_SIZE = 10;
const PROJECTS_POLL_INTERVAL_MS = 60_000;

const useProjectSortQueryParams = () => {
  const { projectSortOrder } = usePreferencesContext((state) => ({
    projectViewMode: state.projectViewMode,
    projectSortOrder: state.projectSortOrder,
  }));
  const params = useMemo(() => {
    return {
      sort: {
        col: projectSortOrder.column,
        dir: projectSortOrder.direction,
      },
    };
  }, [projectSortOrder]);
  return params;
};

export function ProjectsPage() {
  const { timeRangeISOStrings } = useTimeRange();
  const _queryParams = useProjectSortQueryParams();
  // we only want to trigger the initial query when the component mounts
  // so we want to cache the initial state of the query params
  // this prevents full page reloads when the sort order changes
  const [queryParams] = useState(() => _queryParams);
  const data = useLazyLoadQuery<ProjectsPageQuery>(
    graphql`
      query ProjectsPageQuery(
        $first: Int
        $sort: ProjectSort
        $filter: ProjectFilter
      ) {
        ...ProjectsPageProjectsFragment
          @arguments(first: $first, sort: $sort, filter: $filter)
      }
    `,
    {
      first: PAGE_SIZE,
      filter: { value: "", col: "name" },
      ...queryParams,
    },
    { fetchPolicy: "store-and-network" }
  );

  return (
    <>
      <TopNavActions>
        <ConnectedTimeRangeSelector size="S" />
      </TopNavActions>
      <Suspense fallback={<Loading />}>
        <ProjectsPageContent
          timeRangeISOStrings={timeRangeISOStrings}
          query={data}
        />
      </Suspense>
    </>
  );
}

export function ProjectsPageContent({
  timeRangeISOStrings,
  query,
}: {
  timeRangeISOStrings: TimeRangeISOStrings;
  query: ProjectsPageProjectsFragment$key;
}) {
  "use no memo";
  const { projectViewMode } = usePreferencesContext((state) => ({
    projectViewMode: state.projectViewMode,
  }));
  const sortQueryParams = useProjectSortQueryParams();
  const [filter, setFilter] = useState<string>("");
  const notifySuccess = useNotifySuccess();
  const {
    data: projectsData,
    loadNext,
    hasNext,
    isLoadingNext,
    refetch: _refetch,
  } = usePaginationFragment<
    ProjectsPageProjectsQuery,
    ProjectsPageProjectsFragment$key
  >(
    graphql`
      fragment ProjectsPageProjectsFragment on Query
      @refetchable(queryName: "ProjectsPageProjectsQuery")
      @argumentDefinitions(
        after: { type: "String", defaultValue: null }
        first: { type: "Int", defaultValue: 50 }
        sort: { type: "ProjectSort", defaultValue: null }
        filter: { type: "ProjectFilter", defaultValue: null }
      ) {
        projects(first: $first, after: $after, sort: $sort, filter: $filter)
          @connection(key: "ProjectsPage_projects") {
          edges {
            project: node {
              id
              name
              gradientStartColor
              gradientEndColor
              endTime
              startTime
            }
          }
        }
      }
    `,
    query
  );

  const queryArgs = useMemo(
    () => ({
      ...sortQueryParams,
      filter: { value: filter, col: "name" as const },
    }),
    [sortQueryParams, filter]
  );

  const refetch = useCallback(
    ({
      vars,
      onComplete,
    }: {
      vars?: Partial<Parameters<typeof _refetch>[0]>;
      onComplete?: () => void;
    }) => {
      startTransition(() => {
        _refetch(
          {
            ...queryArgs,
            ...vars,
          },
          {
            fetchPolicy: "store-and-network",
            onComplete: () => {
              onComplete?.();
            },
          }
        );
      });
    },
    [_refetch, queryArgs]
  );

  useInterval(() => refetch({}), PROJECTS_POLL_INTERVAL_MS);

  const projects = projectsData?.projects?.edges?.map((p) => p.project);

  const projectsContainerRef = useRef<HTMLDivElement>(null);
  const fetchMoreOnBottomReached = useCallback(
    (containerRefElement?: HTMLDivElement | null) => {
      if (containerRefElement) {
        const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
        //once the user has scrolled within 300px of the bottom of the scrollable container, fetch more data if there is any
        if (
          scrollHeight - scrollTop - clientHeight < 300 &&
          !isLoadingNext &&
          hasNext
        ) {
          loadNext(PAGE_SIZE, { UNSTABLE_extraVariables: queryArgs });
        }
      }
    },
    [hasNext, isLoadingNext, loadNext, queryArgs]
  );

  const onSort = useCallback(
    (sort: ProjectSort) => {
      refetch({
        vars: {
          sort,
        },
      });
    },
    [refetch]
  );

  const onDelete = useCallback(
    (projectName: string) => {
      startTransition(() => {
        refetch({
          onComplete: () => {
            notifySuccess({
              title: "Project Deleted",
              message: `Project ${projectName} has been deleted.`,
            });
          },
        });
      });
    },
    [notifySuccess, refetch]
  );

  const onClear = useCallback(
    (projectName: string) => {
      startTransition(() => {
        refetch({
          onComplete: () => {
            notifySuccess({
              title: "Project Cleared",
              message: `Project ${projectName} has been cleared of traces.`,
            });
          },
        });
      });
    },
    [notifySuccess, refetch]
  );

  const onRemove = useCallback(
    (projectName: string) => {
      startTransition(() => {
        refetch({
          onComplete: () => {
            notifySuccess({
              title: "Project Data Removed",
              message: `Old data from project ${projectName} have been removed.`,
            });
          },
        });
      });
    },
    [notifySuccess, refetch]
  );

  const loadNextWithArgs = useCallback(() => {
    loadNext(PAGE_SIZE, { UNSTABLE_extraVariables: queryArgs });
  }, [loadNext, queryArgs]);

  return (
    <div
      css={css`
        display: flex;
        flex-direction: column;
        overflow: auto;
      `}
      onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
      ref={projectsContainerRef}
    >
      <View
        padding="size-200"
        width="100%"
        borderBottomColor="default"
        borderBottomWidth="thin"
        flex="none"
      >
        <Flex
          direction="row"
          justifyContent="space-between"
          alignItems="center"
          gap="size-100"
        >
          <DebouncedSearch
            aria-label="Search projects by name"
            placeholder="Search projects by name"
            onChange={(newSearch) => {
              setFilter(newSearch);
              refetch({
                vars: {
                  filter: { value: newSearch, col: "name" },
                },
              });
            }}
          />
          <Flex
            direction="row"
            justifyContent="end"
            alignItems="center"
            gap="size-100"
            css={css`
              button {
                text-wrap: nowrap;
              }
            `}
          >
            <ProjectViewModeToggle />
            <CanModify>
              <NewProjectButton
                variant="primary"
                onProjectCreated={() => refetch({})}
              />
            </CanModify>
          </Flex>
        </Flex>
      </View>
      {projectViewMode === "grid" ? (
        <div
          css={css`
            display: flex;
            flex-direction: column;
            flex: 1 1 auto;
          `}
        >
          <ProjectsGrid
            projects={projects}
            onDelete={onDelete}
            onClear={onClear}
            onRemove={onRemove}
            timeRangeISOStrings={timeRangeISOStrings}
            hasNext={hasNext}
            loadNext={loadNextWithArgs}
            isLoadingNext={isLoadingNext}
            onSort={onSort}
          />
        </div>
      ) : (
        <div
          css={css`
            display: flex;
            flex-direction: column;
            flex: 1 1 auto;
          `}
        >
          <ProjectsTable
            projects={projects}
            onDelete={onDelete}
            onClear={onClear}
            onRemove={onRemove}
            timeRangeISOStrings={timeRangeISOStrings}
            hasNext={hasNext}
            loadNext={loadNextWithArgs}
            isLoadingNext={isLoadingNext}
            onSort={onSort}
          />
        </div>
      )}
    </div>
  );
}

type ProjectViewComponentProps = {
  projects: ProjectsPageProjectsFragment$data["projects"]["edges"][number]["project"][];
  onDelete: (projectName: string) => void;
  onClear: (projectName: string) => void;
  onRemove: (projectName: string) => void;
  timeRangeISOStrings: TimeRangeISOStrings;
  hasNext: boolean;
  loadNext: () => void;
  isLoadingNext: boolean;
  onSort: (sort: ProjectSort) => void;
};

function ProjectsGrid({
  projects,
  onDelete,
  onClear,
  onRemove,
  timeRangeISOStrings,
  hasNext,
  loadNext,
  isLoadingNext,
}: ProjectViewComponentProps) {
  const timeRangeSearch = useTimeRangeSearch();
  return (
    <View padding="size-200" width="100%">
      <ul
        css={css`
          display: grid;
          grid-template-columns: repeat(
            auto-fill,
            minmax(var(--global-dimension-size-3600), 1fr)
          );
          gap: var(--global-dimension-size-200);
        `}
      >
        {projects?.map((project) => (
          <li
            key={project.id}
            css={css`
              display: flex;
              flex-direction: column;
              height: 100%;
              & > div {
                height: 100%;
              }
            `}
          >
            <Link
              title={project.name}
              to={`/projects/${project.id}${timeRangeSearch}`}
              css={css`
                text-decoration: none;
                display: flex;
                flex-direction: column;
                height: 100%;
              `}
            >
              <ProjectItem
                project={project}
                timeRange={timeRangeISOStrings}
                onProjectDelete={() => onDelete(project.name)}
                onProjectClear={() => onClear(project.name)}
                onProjectRemoveData={() => onRemove(project.name)}
              />
            </Link>
          </li>
        ))}
      </ul>
      {hasNext && (
        <Flex
          width="100%"
          justifyContent="center"
          alignItems="center"
          marginTop="size-200"
        >
          <LoadMoreButton onLoadMore={loadNext} isLoadingNext={isLoadingNext} />
        </Flex>
      )}
    </View>
  );
}

type ProjectItemProps = {
  project: ProjectsPageProjectsFragment$data["projects"]["edges"][number]["project"];
  onProjectDelete: () => void;
  onProjectClear: () => void;
  onProjectRemoveData: () => void;
  timeRange: {
    start: string | undefined;
    end: string | undefined;
  };
};

function ProjectItem({
  project,
  onProjectDelete,
  onProjectClear,
  onProjectRemoveData,
  timeRange,
}: ProjectItemProps) {
  const { gradientStartColor, gradientEndColor, endTime } = project;
  const lastUpdatedText = useMemo(() => {
    if (endTime) {
      return `Last updated  ${formatDistance(new Date(endTime), new Date(), { addSuffix: true })}`;
    }
    return "No traces uploaded yet.";
  }, [endTime]);
  return (
    <div
      css={css`
        padding: var(--global-dimension-size-200);
        border: 1px solid var(--global-color-gray-100);
        box-shadow:
          0 0 1px 0px var(--global-color-gray-400) inset,
          0 0 1px 0px var(--global-color-gray-400);
        border-radius: var(--global-rounding-medium);
        transition: border-color 0.2s;
        &:hover {
          border-color: var(--global-color-primary);
        }
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        gap: var(--global-dimension-size-200);
        height: 100%;
      `}
    >
      <Flex
        direction="row"
        justifyContent="space-between"
        alignItems="start"
        gap="size-100"
      >
        <Flex
          direction="row"
          gap="size-100"
          alignItems="center"
          flex={1}
          minWidth={0}
        >
          <GradientCircle
            gradientStartColor={gradientStartColor}
            gradientEndColor={gradientEndColor}
          />
          <Flex direction="column" flex={1} minWidth={0}>
            <Heading
              level={2}
              css={css`
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
              `}
            >
              {project.name}
            </Heading>
            <Text color="text-700" size="XS" fontStyle="italic">
              {lastUpdatedText}
            </Text>
          </Flex>
        </Flex>
        <CanModify>
          <ProjectActionMenu
            projectId={project.id}
            projectName={project.name}
            onProjectDelete={onProjectDelete}
            onProjectClear={onProjectClear}
            onProjectRemoveData={onProjectRemoveData}
          />
        </CanModify>
      </Flex>
      <ProjectMetrics projectId={project.id} timeRange={timeRange} />
    </div>
  );
}

const PROJECT_METRICS_QUERY = graphql`
  query ProjectsPageProjectMetricsQuery($id: ID!, $timeRange: TimeRange!) {
    project: node(id: $id) {
      ... on Project {
        traceCount(timeRange: $timeRange)
        sessionCount(timeRange: $timeRange)
        latencyMsP50: latencyMsQuantile(probability: 0.5, timeRange: $timeRange)
      }
    }
  }
`;

function ProjectMetricsLoadingSkeleton() {
  return (
    <Flex direction="row" justifyContent="space-between" minHeight="size-600">
      <Flex direction="column" flex="none" gap="size-100">
        <Text elementType="h3" size="S" color="text-700">
          Traces
        </Text>
        <Skeleton width={60} height={20} animation="wave" />
      </Flex>
      <Flex direction="column" flex="none" gap="size-100">
        <Text elementType="h3" size="S" color="text-700">
          Sessions
        </Text>
        <Skeleton width={60} height={20} animation="wave" />
      </Flex>
      <Flex direction="column" flex="none" gap="size-100">
        <Text elementType="h3" size="S" color="text-700">
          Latency P50
        </Text>
        <Skeleton width={60} height={20} animation="wave" />
      </Flex>
    </Flex>
  );
}

const ProjectMetrics = memo(function ProjectMetrics({
  projectId,
  timeRange,
  flexProps,
}: {
  projectId: string;
  timeRange: {
    start: string | undefined;
    end: string | undefined;
  };
  flexProps?: Partial<FlexProps>;
}) {
  const environment = useRelayEnvironment();
  // ref to the current running "subscription", a.k.a. the current running query request
  const subscriptionRef = useRef<Subscription | null>(null);
  // state to hold the result of the project metrics query
  const [projectMetrics, setProjectMetrics] =
    useState<ProjectsPageProjectMetricsQuery$data | null>(null);
  /**
   * fetchProject is a function that fetches the project metrics for the given project id and time range
   * it returns a "subscription" object that can be used to unsubscribe from the query
   * It is NOT websockets, polling, or streaming, it is a normal fetch wrapped in a relay subscription provided by the relay environment
   * this works because the relay environment is configured to abort the underlying fetch if the subscription is unsubscribed from
   */
  const fetchProject = useCallback(() => {
    const observable = fetchQuery<ProjectsPageProjectMetricsQuery>(
      environment,
      PROJECT_METRICS_QUERY,
      { id: projectId, timeRange }
    );
    const subscription = observable.subscribe({
      next: (data) => {
        setProjectMetrics(data);
      },
      error: () => {
        setProjectMetrics(null);
      },
    });
    return subscription;
  }, [projectId, timeRange, environment]);
  // when the component mounts, or the time range changes, we fetch the project metrics
  useEffect(() => {
    subscriptionRef.current = fetchProject();
    return () => {
      subscriptionRef.current?.unsubscribe();
      subscriptionRef.current = null;
    };
  }, [fetchProject]);
  // if the project metrics are not loaded yet, we show a loading indicator
  if (projectMetrics == null) {
    return <ProjectMetricsLoadingSkeleton />;
  }
  // if the project metrics are loaded, we show the project metrics
  return <ProjectMetricsRow project={projectMetrics} flexProps={flexProps} />;
});

function ProjectMetricsRow({
  project,
  flexProps,
}: {
  project: ProjectsPageProjectMetricsQuery$data;
  flexProps?: Partial<FlexProps>;
}) {
  const {
    project: { traceCount, sessionCount, latencyMsP50 },
  } = project;
  return (
    <Flex
      direction="row"
      justifyContent="space-between"
      minHeight="size-600"
      {...flexProps}
    >
      <Flex direction="column">
        <Text elementType="h3" size="S" color="text-700">
          Traces
        </Text>
        <Text size="L" fontFamily="mono">
          {intFormatter(traceCount)}
        </Text>
      </Flex>
      <Flex direction="column">
        <Text elementType="h3" size="S" color="text-700">
          Sessions
        </Text>
        <Text size="L" fontFamily="mono">
          {intFormatter(sessionCount)}
        </Text>
      </Flex>
      <Flex direction="column">
        <Text elementType="h3" size="S" color="text-700">
          Latency P50
        </Text>
        {latencyMsP50 != null ? (
          <LatencyText latencyMs={latencyMsP50} size="L" />
        ) : (
          <Text size="L">--</Text>
        )}
      </Flex>
    </Flex>
  );
}

const SORT_COLUMNS = ["name", "endTime"] satisfies ProjectSortOrder["column"][];

function ProjectsTable({
  projects,
  onDelete,
  onClear,
  onRemove,
  timeRangeISOStrings,
  hasNext,
  loadNext,
  isLoadingNext,
  onSort,
}: ProjectViewComponentProps) {
  const navigate = useNavigate();
  const canModify = useViewerCanModify();
  const timeRangeSearch = useTimeRangeSearch();
  const columns: ColumnDef<
    ProjectsPageProjectsFragment$data["projects"]["edges"][number]["project"]
  >[] = useMemo(() => {
    const cols: ColumnDef<
      ProjectsPageProjectsFragment$data["projects"]["edges"][number]["project"]
    >[] = [
      {
        header: "name",
        accessorKey: "name",
        maxSize: 135,
        cell: ({ row }) => {
          return (
            <Flex direction="row" gap="size-200" alignItems="center">
              <GradientCircle
                gradientStartColor={row.original.gradientStartColor}
                gradientEndColor={row.original.gradientEndColor}
              />
              <Link to={`/projects/${row.original.id}${timeRangeSearch}`}>
                <Truncate maxWidth="300px">{row.original.name}</Truncate>
              </Link>
            </Flex>
          );
        },
      },
      {
        header: "last updated at",
        accessorKey: "endTime",
        maxSize: 30,
        cell: TimestampCell,
      },
      {
        header: "metrics",
        id: "metrics",
        enableSorting: false,
        cell: ({ row }) => {
          return (
            <div
              css={css`
                max-width: 100%;
              `}
            >
              <ProjectMetrics
                flexProps={{
                  justifyContent: "start",
                  gap: "size-800",
                  rowGap: "size-100",
                }}
                projectId={row.original.id}
                timeRange={timeRangeISOStrings}
              />
            </div>
          );
        },
      },
    ];
    if (canModify) {
      cols.push({
        header: "",
        id: "actions",
        enableSorting: false,
        size: 10,
        maxSize: 10,
        minSize: 10,
        cell: ({ row }) => {
          return (
            <ProjectActionMenu
              projectId={row.original.id}
              projectName={row.original.name}
              onProjectClear={() => onClear(row.original.name)}
              onProjectRemoveData={() => onRemove(row.original.name)}
              onProjectDelete={() => onDelete(row.original.name)}
            />
          );
        },
      });
    }
    return cols;
  }, [
    timeRangeISOStrings,
    timeRangeSearch,
    onClear,
    onDelete,
    onRemove,
    canModify,
  ]);
  const sortQueryParams = useProjectSortQueryParams();
  const { setProjectSortOrder } = usePreferencesContext((state) => ({
    setProjectSortOrder: state.setProjectSortOrder,
  }));
  const sortingRowModel = useMemo(() => {
    return [
      {
        id: sortQueryParams.sort.col,
        desc: sortQueryParams.sort.dir === "desc",
      },
    ] satisfies SortingState;
  }, [sortQueryParams.sort.col, sortQueryParams.sort.dir]);
  const onSortingChange: OnChangeFn<SortingState> = useCallback(
    (updater) => {
      if (typeof updater === "function") {
        const sorting = updater(sortingRowModel);
        const [first] = sorting;
        if (first == null) {
          return;
        }
        const column = first.id as (typeof SORT_COLUMNS)[number];
        if (!SORT_COLUMNS.includes(column)) {
          return;
        }
        onSort({
          col: column,
          dir: first.desc ? "desc" : "asc",
        });
        setProjectSortOrder({
          column,
          direction: first.desc ? "desc" : "asc",
        });
      }
    },
    [setProjectSortOrder, sortingRowModel, onSort]
  );
  // eslint-disable-next-line react-hooks-js/incompatible-library
  const table = useReactTable({
    data: projects,
    columns,
    getCoreRowModel: getCoreRowModel(),
    state: {
      sorting: sortingRowModel,
    },
    enableSortingRemoval: false,
    onSortingChange,
  });
  const rows = table.getRowModel().rows;
  const isEmpty = rows.length === 0;
  return (
    <View flex="none" height="100%" width="100%">
      <div
        css={css`
          flex: 1 1 auto;
          height: 100%;
          width: 100%;
        `}
      >
        <table css={selectableTableCSS} data-testid="projects-table">
          <thead>
            {table.getHeaderGroups().map((headerGroup) => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  const sortDir = header.column.getIsSorted();
                  return (
                    <th
                      colSpan={header.colSpan}
                      key={header.id}
                      aria-sort={
                        sortDir === "asc"
                          ? "ascending"
                          : sortDir === "desc"
                            ? "descending"
                            : header.column.getCanSort()
                              ? "none"
                              : undefined
                      }
                      style={{
                        width: header.getSize(),
                      }}
                    >
                      {header.isPlaceholder ? null : (
                        <div
                          {...{
                            className: header.column.getCanSort() ? "sort" : "",
                            onClick: header.column.getToggleSortingHandler(),
                            style: {
                              left: header.getStart(),
                              width: header.getSize(),
                              textWrap: "nowrap",
                            },
                          }}
                        >
                          {flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                          {header.column.getIsSorted() ? (
                            <Icon
                              className="sort-icon"
                              svg={
                                header.column.getIsSorted() === "asc" ? (
                                  <Icons.CaretUpFilled />
                                ) : (
                                  <Icons.CaretDownFilled />
                                )
                              }
                            />
                          ) : null}
                        </div>
                      )}
                    </th>
                  );
                })}
              </tr>
            ))}
          </thead>
          {isEmpty ? (
            <TableEmptyWrap>
              <EmptyState
                graphic={<EmptyStateGraphic variant="project" />}
                description="No projects found"
              />
            </TableEmptyWrap>
          ) : (
            <tbody>
              {table.getRowModel().rows.map((row) => (
                <tr
                  key={row.id}
                  onClick={() => {
                    navigate(`/projects/${row.original.id}${timeRangeSearch}`);
                  }}
                >
                  {row.getVisibleCells().map((cell) => (
                    <td
                      key={cell.id}
                      style={{
                        width: cell.column.getSize(),
                        maxWidth: cell.column.getSize(),
                      }}
                    >
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          )}
        </table>
        {hasNext ? (
          <View paddingY="size-200">
            <Flex
              direction="row"
              justifyContent="center"
              alignItems="center"
              width="100%"
            >
              <LoadMoreButton
                onLoadMore={loadNext}
                isLoadingNext={isLoadingNext}
              />
            </Flex>
          </View>
        ) : null}
      </div>
    </View>
  );
}
