| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- // @ts-nocheck
- import { keepPreviousData } from '@tanstack/react-query'
- import { useDebounce } from '@uidotdev/usehooks'
- import { LOCAL_STORAGE_KEYS, useParams } from 'common'
- import { parseAsArrayOf, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
- import { useMemo } from 'react'
- import {
- Card,
- cn,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableHeadSort,
- TableRow,
- } from 'ui'
- import { LoadingCardView, LoadingTableView, NoProjectsState } from './EmptyStates'
- import { LoadMoreRows } from './LoadMoreRow'
- import { ProjectCard } from './ProjectCard'
- import {
- getNextProjectListSortForColumn,
- getProjectListAriaSort,
- PROJECT_LIST_SORT_VALUES,
- toTableHeadSortValue,
- } from './ProjectListSort.utils'
- import { ProjectTableRow } from './ProjectTableRow'
- import AlertError from '@/components/ui/AlertError'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
- import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-org-only'
- import { usePermissionsQuery } from '@/data/permissions/permissions-query'
- import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
- import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { IS_PLATFORM } from '@/lib/constants'
- import type { Organization } from '@/types'
- export interface ProjectListProps {
- organization?: Organization
- rewriteHref?: (projectRef: string) => string
- }
- export const ProjectList = ({ organization: organization_, rewriteHref }: ProjectListProps) => {
- const { slug: urlSlug } = useParams()
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const [search] = useQueryState('search', parseAsString.withDefault(''))
- const debouncedSearch = useDebounce(search, 500)
- const [filterStatus, setFilterStatus] = useQueryState(
- 'status',
- parseAsArrayOf(parseAsString, ',').withDefault([])
- )
- const [sort, setSort] = useQueryState(
- 'sort',
- parseAsStringLiteral(PROJECT_LIST_SORT_VALUES).withDefault('name_asc')
- )
- const [viewMode] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.PROJECTS_VIEW, 'grid')
- const organization = organization_ ?? selectedOrganization
- const slug = organization?.slug ?? urlSlug
- const {
- data,
- error: projectsError,
- isLoading: isLoadingProjects,
- isSuccess: isSuccessProjects,
- isError: isErrorProjects,
- isFetchingNextPage,
- hasNextPage,
- fetchNextPage,
- } = useOrgProjectsInfiniteQuery(
- {
- slug,
- sort,
- search: search.length === 0 ? search : debouncedSearch,
- statuses: filterStatus,
- },
- {
- placeholderData: keepPreviousData,
- }
- )
- const orgProjects =
- useMemo(() => data?.pages.flatMap((page) => page.projects), [data?.pages]) || []
- const {
- isPending: _isLoadingPermissions,
- isError: isErrorPermissions,
- error: permissionsError,
- } = usePermissionsQuery()
- const { data: resourceWarnings } = useResourceWarningsQuery({ slug })
- // Move all hooks to the top to comply with Rules of Hooks
- const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
- const { data: connections } = useGitHubConnectionsQuery({ organizationId: organization?.id })
- const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
- const isEmpty =
- debouncedSearch.length === 0 &&
- filterStatus.length === 0 &&
- (!orgProjects || orgProjects.length === 0)
- const noResultsFromSearch =
- debouncedSearch.length > 0 && isSuccessProjects && orgProjects.length === 0
- const noResultsFromStatusFilter =
- filterStatus.length > 0 && isSuccessProjects && orgProjects.length === 0
- const noResults = noResultsFromStatusFilter || noResultsFromSearch
- const tableHeadSortValue = toTableHeadSortValue(sort)
- const githubConnections = connections?.map((connection) => ({
- id: String(connection.id),
- added_by: {
- id: String(connection.user?.id),
- primary_email: connection.user?.primary_email ?? '',
- username: connection.user?.username ?? '',
- },
- foreign_project_id: String(connection.repository.id),
- briven_project_ref: connection.project.ref,
- organization_integration_id: 'unused',
- inserted_at: connection.inserted_at,
- updated_at: connection.updated_at,
- metadata: {
- name: connection.repository.name,
- } as any,
- }))
- const vercelConnections = integrations
- ?.filter((integration) => integration.integration.name === 'Vercel')
- .flatMap((integration) => integration.connections)
- if (isErrorPermissions) {
- return (
- <AlertError
- subject="Failed to retrieve permissions for your account"
- error={permissionsError}
- />
- )
- }
- if (isErrorProjects) {
- return (
- <AlertError
- subject={`Failed to retrieve projects under ${organization?.name}`}
- error={projectsError}
- />
- )
- }
- if (isLoadingPermissions || isLoadingProjects || !organization) {
- return viewMode === 'table' ? <LoadingTableView /> : <LoadingCardView />
- }
- if (isEmpty) {
- return <NoProjectsState slug={organization?.slug ?? ''} />
- }
- if (viewMode === 'table') {
- return (
- <Card className="flex-1 min-h-0 overflow-y-auto mb-8">
- <Table>
- {/* [Joshen] Ideally we can figure out sticky table headers here */}
- <TableHeader>
- <TableRow>
- <TableHead
- className={cn(noResults && 'text-foreground-muted')}
- aria-sort={getProjectListAriaSort(sort)}
- >
- <TableHeadSort
- column="name"
- currentSort={tableHeadSortValue}
- onSortChange={() => {
- const sortValue = sort.includes('created')
- ? 'name_asc'
- : getNextProjectListSortForColumn(sort)
- setSort(sortValue)
- }}
- className={cn(noResults && 'text-foreground-muted')}
- >
- Project
- </TableHeadSort>
- </TableHead>
- <TableHead className={cn(noResults && 'text-foreground-muted')}>Status</TableHead>
- <TableHead className={cn(noResults && 'text-foreground-muted')}>Compute</TableHead>
- <TableHead className={cn(noResults && 'text-foreground-muted')}>Region</TableHead>
- <TableHead
- className={cn(noResults && 'text-foreground-muted')}
- aria-sort={getProjectListAriaSort(sort)}
- >
- <TableHeadSort
- column="created"
- currentSort={tableHeadSortValue}
- onSortChange={() => {
- const sortValue = sort.includes('name')
- ? 'created_asc'
- : getNextProjectListSortForColumn(sort)
- setSort(sortValue)
- }}
- className={cn(noResults && 'text-foreground-muted')}
- >
- Created
- </TableHeadSort>
- </TableHead>
- <TableHead className={cn(noResults && 'text-foreground-muted')} />
- </TableRow>
- </TableHeader>
- <TableBody>
- {noResultsFromStatusFilter ? (
- <TableRow className="[&>td]:hover:bg-inherit">
- <TableCell colSpan={6}>
- <NoSearchResults
- withinTableCell
- label={
- filterStatus.length === 0
- ? `No projects found`
- : `No ${filterStatus[0] === 'INACTIVE' ? 'paused' : 'active'} projects found`
- }
- description="Your search for projects with the specified status did not return any results"
- onResetFilter={() => setFilterStatus([])}
- />
- </TableCell>
- </TableRow>
- ) : noResultsFromSearch ? (
- <TableRow className="[&>td]:hover:bg-inherit">
- <TableCell colSpan={6}>
- <NoSearchResults searchString={search} withinTableCell />
- </TableCell>
- </TableRow>
- ) : (
- <>
- {orgProjects?.map((project) => (
- <ProjectTableRow
- key={project.ref}
- project={project}
- organization={organization}
- rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
- resourceWarnings={resourceWarnings?.find(
- (resourceWarning) => resourceWarning.project === project.ref
- )}
- githubIntegration={githubConnections?.find(
- (connection) => connection.briven_project_ref === project.ref
- )}
- vercelIntegration={vercelConnections?.find(
- (connection) => connection.briven_project_ref === project.ref
- )}
- />
- ))}
- {hasNextPage && (
- <LoadMoreRows
- type="table"
- isFetchingNextPage={isFetchingNextPage}
- fetchNextPage={fetchNextPage}
- />
- )}
- </>
- )}
- </TableBody>
- </Table>
- </Card>
- )
- }
- return (
- <>
- {noResultsFromStatusFilter ? (
- <NoSearchResults
- label={
- filterStatus.length === 0
- ? `No projects found`
- : `No ${filterStatus[0] === 'INACTIVE' ? 'paused' : 'active'} projects found`
- }
- description="Your search for projects with the specified status did not return any results"
- onResetFilter={() => setFilterStatus([])}
- />
- ) : noResultsFromSearch ? (
- <NoSearchResults searchString={search} />
- ) : (
- <div className="flex flex-col gap-y-2 md:gap-y-4 pb-6">
- <ul
- className={cn(
- 'min-h-0 w-full mx-auto',
- 'grid grid-cols-1 gap-2 md:gap-4',
- 'sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
- )}
- >
- {orgProjects?.map((project) => (
- <ProjectCard
- key={project.ref}
- slug={slug}
- project={project}
- rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
- resourceWarnings={resourceWarnings?.find(
- (resourceWarning) => resourceWarning.project === project.ref
- )}
- githubIntegration={githubConnections?.find(
- (connection) => connection.briven_project_ref === project.ref
- )}
- vercelIntegration={vercelConnections?.find(
- (connection) => connection.briven_project_ref === project.ref
- )}
- />
- ))}
- </ul>
- {hasNextPage && (
- <LoadMoreRows
- type="card"
- isFetchingNextPage={isFetchingNextPage}
- fetchNextPage={fetchNextPage}
- />
- )}
- </div>
- )}
- </>
- )
- }
|