Files
supabase/apps/studio/components/interfaces/Home/ProjectList/ProjectList.tsx
2024-03-04 20:48:22 +08:00

242 lines
8.2 KiB
TypeScript

import { groupBy } from 'lodash'
import Link from 'next/link'
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 {
OverdueInvoicesResponse,
useOverdueInvoicesQuery,
} from 'data/invoices/invoices-overdue-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { ProjectInfo, useProjectsQuery } from 'data/projects/projects-query'
import { ResourceWarning, useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
import { useSelectedOrganization } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import { makeRandomString } from 'lib/helpers'
import type { Organization, ResponseError } from 'types'
import { Button, IconPlus } from 'ui'
import ProjectCard from './ProjectCard'
import ShimmeringCard from './ShimmeringCard'
export interface ProjectListProps {
rewriteHref?: (projectRef: string) => string
search: string
}
const ProjectList = ({ search, rewriteHref }: ProjectListProps) => {
const { data: organizations, isLoading, isSuccess } = useOrganizationsQuery()
const {
data: allProjects,
isLoading: isLoadingProjects,
isError: isErrorProjects,
error: projectsError,
} = useProjectsQuery()
const {
isLoading: _isLoadingPermissions,
isError: isErrorPermissions,
error: permissionsError,
} = usePermissionsQuery()
const { data: resourceWarnings } = useResourceWarningsQuery()
const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: IS_PLATFORM })
const projectsByOrg = groupBy(allProjects, 'organization_id')
const isLoadingPermissions = IS_PLATFORM ? _isLoadingPermissions : false
const noResults =
search.length > 0 &&
allProjects !== undefined &&
allProjects.filter((project) => {
return (
project.name.toLowerCase().includes(search.toLowerCase()) ||
project.ref.includes(search.toLowerCase())
)
}).length === 0
if (isLoading) {
return (
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<ShimmeringCard />
<ShimmeringCard />
</ul>
)
}
if (noResults) {
return <NoSearchResults searchString={search} />
}
return isSuccess && organizations && organizations?.length > 0 ? (
<>
{organizations?.map((organization) => {
return (
<OrganizationProjects
key={organization.id}
organization={organization}
projects={projectsByOrg[organization.id]}
overdueInvoices={(allOverdueInvoices ?? []).filter(
(it) => it.organization_id === organization.id
)}
resourceWarnings={resourceWarnings ?? []}
rewriteHref={rewriteHref}
isLoadingPermissions={isLoadingPermissions}
isErrorPermissions={isErrorPermissions}
permissionsError={permissionsError}
isLoadingProjects={isLoadingProjects}
isErrorProjects={isErrorProjects}
projectsError={projectsError}
search={search}
/>
)
})}
</>
) : (
<NoProjectsState slug={''} />
)
}
export default ProjectList
type OrganizationProjectsProps = {
organization: Organization
projects: ProjectInfo[]
overdueInvoices: OverdueInvoicesResponse[]
resourceWarnings: ResourceWarning[]
isLoadingPermissions: boolean
isErrorPermissions: boolean
permissionsError: ResponseError | null
isLoadingProjects: boolean
isErrorProjects: boolean
projectsError: ResponseError | null
rewriteHref?: (projectRef: string) => string
search: string
}
const OrganizationProjects = ({
organization: { name, slug },
projects,
overdueInvoices,
resourceWarnings,
isLoadingPermissions,
isErrorPermissions,
permissionsError,
isLoadingProjects,
isErrorProjects,
projectsError,
rewriteHref,
search,
}: OrganizationProjectsProps) => {
const organization = useSelectedOrganization()
const isEmpty = !projects || projects.length === 0
const sortedProjects = [...(projects || [])].sort((a, b) => a.name.localeCompare(b.name))
const filteredProjects =
search.length > 0
? sortedProjects.filter((project) => {
return (
project.name.toLowerCase().includes(search.toLowerCase()) ||
project.ref.includes(search.toLowerCase())
)
})
: sortedProjects
const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
const { data: connections } = useGitHubConnectionsQuery({ organizationId: organization?.id })
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),
supabase_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 (search.length > 0 && filteredProjects.length === 0) return null
return (
<div className="space-y-3" key={makeRandomString(5)}>
<div className="flex space-x-4 items-center">
<h4 className="text-lg flex items-center">{name}</h4>
{!!overdueInvoices.length && (
<div>
<Button asChild type="danger">
<Link href={`/org/${slug}/invoices`}>Outstanding Invoices</Link>
</Button>
</div>
)}
</div>
{isLoadingPermissions || isLoadingProjects ? (
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<ShimmeringCard />
<ShimmeringCard />
</ul>
) : (
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
{isErrorPermissions ? (
<div className="col-span-3">
<AlertError
subject="Failed to retrieve permissions for your account"
error={permissionsError}
/>
</div>
) : isErrorProjects ? (
<div className="col-span-3">
<AlertError
subject={`Failed to retrieve projects under ${name}`}
error={projectsError}
/>
</div>
) : isEmpty ? (
<NoProjectsState slug={slug} />
) : (
filteredProjects?.map((project) => (
<ProjectCard
key={makeRandomString(5)}
project={project}
rewriteHref={rewriteHref ? rewriteHref(project.ref) : undefined}
resourceWarnings={resourceWarnings.find(
(resourceWarning) => resourceWarning.project === project.ref
)}
githubIntegration={githubConnections?.find(
(connection) => connection.supabase_project_ref === project.ref
)}
vercelIntegration={vercelConnections?.find(
(connection) => connection.supabase_project_ref === project.ref
)}
/>
))
)}
</ul>
)}
</div>
)
}
const NoProjectsState = ({ slug }: { slug: string }) => {
return (
<div className="col-span-4 space-y-4 rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
<div className="space-y-1">
<p>No projects</p>
<p className="text-sm text-foreground-light">Get started by creating a new project.</p>
</div>
<div>
<Button asChild icon={<IconPlus />}>
<Link href={`/new/${slug}`}>New Project</Link>
</Button>
</div>
</div>
)
}