mirror of
https://github.com/supabase/supabase.git
synced 2026-06-06 05:17:15 +08:00
chore(studio): improve projects presentation (#41179)
* improve admonition * remove redundant loader * copywriting * table empty states * improve integrations presentation * match integration styling * fix clipping and h-full container * revert test * improve status presentation * nicer status * card status improvements * comment clarification * prettier lint * fix testes * comment * Fix infinite loading behaviour * Clean u0p * Replace NoFilterResults with NoSearchResults --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
@@ -42,6 +42,11 @@ A [Table](../components/table) instance with zero results should display a singl
|
||||
|
||||
<ComponentPreview name="empty-state-zero-items-table" peekCode wide />
|
||||
|
||||
Studio contains two pre-built components to handle these cases consistently:
|
||||
|
||||
- No Filter Results
|
||||
- No Search Results
|
||||
|
||||
#### Data Grid
|
||||
|
||||
[Data Grid](../ui-patterns/tables#data-grid) and [Data Table](../ui-patterns/tables#data-table) component patterns typically span the full height and width of a container. A classic example is [Users](https://supabase.com/dashboard/project/_/auth/users), which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration.
|
||||
|
||||
@@ -7,7 +7,6 @@ import { BASE_PATH } from 'lib/constants'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
cn,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -36,62 +35,6 @@ export const Header = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const NoFilterResults = ({
|
||||
filterStatus,
|
||||
resetFilterStatus,
|
||||
className,
|
||||
}: {
|
||||
filterStatus: string[]
|
||||
resetFilterStatus?: () => void
|
||||
className?: string
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-surface-100 px-4 md:px-6 py-4 rounded flex items-center justify-between border border-default',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{/* [Joshen] Just keeping it simple for now unless we decide to extend this to other statuses */}
|
||||
<p className="text-sm text-foreground">
|
||||
{filterStatus.length === 0
|
||||
? `No projects found`
|
||||
: `No ${filterStatus[0] === 'INACTIVE' ? 'paused' : 'active'} projects found`}
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for projects with the specified status did not return any results
|
||||
</p>
|
||||
</div>
|
||||
{resetFilterStatus !== undefined && (
|
||||
<Button type="default" onClick={() => resetFilterStatus()}>
|
||||
Reset filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingTableRow = () => (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-32"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-16"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-24"></Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
|
||||
export const LoadingTableView = () => {
|
||||
return (
|
||||
<Card>
|
||||
@@ -107,7 +50,23 @@ export const LoadingTableView = () => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<LoadingTableRow key={i} />
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-32"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-16"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-24"></Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -142,7 +101,7 @@ export const NoProjectsState = ({ slug }: { slug: string }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const NoOrganizationsState = ({}) => {
|
||||
export const NoOrganizationsState = () => {
|
||||
return (
|
||||
<EmptyStatePresentational
|
||||
title="Create an organization"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useIntersectionObserver } from '@uidotdev/usehooks'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { cn, Skeleton, TableCell, TableRow } from 'ui'
|
||||
import { ShimmeringCard } from './ShimmeringCard'
|
||||
|
||||
interface LoadMoreRowProps {
|
||||
type?: 'card' | 'table'
|
||||
isFetchingNextPage: boolean
|
||||
fetchNextPage: () => void
|
||||
}
|
||||
|
||||
export const LoadMoreRows = ({ type, isFetchingNextPage, fetchNextPage }: LoadMoreRowProps) => {
|
||||
const [sentinelRef, entry] = useIntersectionObserver({
|
||||
threshold: 0,
|
||||
rootMargin: '200px 0px 200px 0px',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (entry?.isIntersecting && !isFetchingNextPage) {
|
||||
fetchNextPage?.()
|
||||
}
|
||||
}, [entry?.isIntersecting, isFetchingNextPage, fetchNextPage])
|
||||
|
||||
if (type === 'card') {
|
||||
return (
|
||||
<ul
|
||||
ref={sentinelRef}
|
||||
className={cn(
|
||||
'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 pb-6'
|
||||
)}
|
||||
>
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<ShimmeringCard key={i} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow ref={sentinelRef}>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-32"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-16"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-20"></Skeleton>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="bg-surface-400 h-4 w-24"></Skeleton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
@@ -53,9 +53,13 @@ export const ProjectCard = ({
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
className="h-44 !px-0 group pt-5 pb-0"
|
||||
title={
|
||||
<div className="w-full justify-between space-y-1.5 px-5">
|
||||
<p className="flex-shrink truncate text-sm pr-4">{name}</p>
|
||||
<span className="text-sm text-foreground-light">{desc}</span>
|
||||
<div className="w-full flex flex-col gap-y-4 justify-between px-5">
|
||||
{/* Text */}
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<h5 className="text-sm flex-shrink truncate pr-5">{name}</h5>
|
||||
<p className="text-sm text-foreground-lighter">{desc}</p>
|
||||
</div>
|
||||
{/* Compute and integrations */}
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{project.status !== 'INACTIVE' && projectHomepageShowInstanceSize && (
|
||||
<ComputeBadgeWrapper
|
||||
@@ -66,7 +70,7 @@ export const ProjectCard = ({
|
||||
/>
|
||||
)}
|
||||
{isVercelIntegrated && (
|
||||
<div className="w-fit p-1 border rounded-md flex items-center text-black dark:text-white">
|
||||
<div className="bg-surface-100 w-5 h-5 p-1 border border-strong rounded-md flex items-center justify-center text-black dark:text-white">
|
||||
<InlineSVG
|
||||
src={`${BASE_PATH}/img/icons/vercel-icon.svg`}
|
||||
title="Vercel Icon"
|
||||
@@ -75,12 +79,12 @@ export const ProjectCard = ({
|
||||
</div>
|
||||
)}
|
||||
{isGithubIntegrated && (
|
||||
<>
|
||||
<div className="w-fit p-1 border rounded-md flex items-center">
|
||||
<div className="bg-surface-100 flex items-center gap-x-0.5 h-5 pr-1 border border-strong rounded-md">
|
||||
<div className="w-5 h-5 p-1 flex items-center justify-center">
|
||||
<Github size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-xs !ml-2 text-foreground-light truncate">{githubRepository}</p>
|
||||
</>
|
||||
<p className="text-xs text-foreground-light truncate">{githubRepository}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,13 @@ import { AlertTriangle, Info, PauseCircle, RefreshCcw } from 'lucide-react'
|
||||
import { RESOURCE_WARNING_MESSAGES } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants'
|
||||
import { getWarningContent } from 'components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils'
|
||||
import type { ResourceWarning } from 'data/usage/resource-warnings-query'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Badge,
|
||||
cn,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { InferredProjectStatus } from './ProjectCard.utils'
|
||||
|
||||
export interface ProjectCardWarningsProps {
|
||||
resourceWarnings?: ResourceWarning
|
||||
projectStatus: InferredProjectStatus
|
||||
renderMode?: 'alert' | 'badge' // New prop to control rendering mode
|
||||
renderMode?: 'alert' | 'badge'
|
||||
}
|
||||
|
||||
export const ProjectCardStatus = ({
|
||||
@@ -130,12 +122,20 @@ export const ProjectCardStatus = ({
|
||||
projectStatus === 'isHealthy'
|
||||
) {
|
||||
if (renderMode === 'badge') {
|
||||
return <Badge variant="success">Active</Badge>
|
||||
return (
|
||||
// Badge must be wrapped in a div in order to be centered in table cell
|
||||
<div className="flex items-center">
|
||||
<Badge variant="success">Active</Badge>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (renderMode === 'badge') {
|
||||
// Render a fallback en dash if no title is available
|
||||
if (!alertTitle) return <span className="text-xs text-foreground-muted">–</span>
|
||||
|
||||
const badgeVariant = isCritical
|
||||
? 'destructive'
|
||||
: activeWarnings.length > 0 ||
|
||||
@@ -146,50 +146,60 @@ export const ProjectCardStatus = ({
|
||||
? 'success'
|
||||
: 'default'
|
||||
|
||||
return alertDescription ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className="rounded-md" variant={badgeVariant}>
|
||||
{alertTitle}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{alertDescription}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge className="rounded-md" variant={badgeVariant}>
|
||||
{alertTitle}
|
||||
</Badge>
|
||||
return (
|
||||
// Badge must be wrapped in a div in order to be centered in table cell
|
||||
<div className="flex items-center">
|
||||
{alertDescription ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant={badgeVariant}>{alertTitle}</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{alertDescription}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge variant={badgeVariant}>{alertTitle}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Only render if an alert title is available
|
||||
if (!alertTitle) return null
|
||||
|
||||
return (
|
||||
<Alert_Shadcn_
|
||||
variant={alertType}
|
||||
className={cn(
|
||||
'border-0 p-5 pb-[1.25rem]',
|
||||
'bg-transparent',
|
||||
'[&>svg]:left-[1.25rem] [&>svg]:top-3.5 [&>svg]:border',
|
||||
!isCritical ? '[&>svg]:text-foreground [&>svg]:bg-surface-100' : ''
|
||||
)}
|
||||
>
|
||||
{['isPaused', 'isPausing'].includes(projectStatus ?? '') ? (
|
||||
<PauseCircle strokeWidth={1.5} size={12} />
|
||||
) : ['isRestoring', 'isComingUp', 'isRestarting', 'isResizing'].includes(
|
||||
projectStatus ?? ''
|
||||
) ? (
|
||||
<RefreshCcw strokeWidth={1.5} size={12} />
|
||||
) : (
|
||||
<AlertTriangle strokeWidth={1.5} size={12} />
|
||||
)}
|
||||
<div className="flex justify-between items-center w-full gap-x-1">
|
||||
<AlertTitle_Shadcn_ className="text-xs mb-0">{alertTitle}</AlertTitle_Shadcn_>
|
||||
<div role="alert" className={cn('w-full p-5 pb-[1.25rem] flex flex-row gap-x-2 items-center')}>
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 w-6 h-6 border rounded-md flex items-center justify-center',
|
||||
alertType === 'destructive' && 'border-destructive-400 [&>svg]:text-destructive-600',
|
||||
alertType === 'warning' && 'border-warning-400 [&>svg]:text-warning-600',
|
||||
alertType === 'default' && 'border-strong [&>svg]:text-foreground'
|
||||
)}
|
||||
>
|
||||
{['isPaused', 'isPausing'].includes(projectStatus ?? '') ? (
|
||||
<PauseCircle strokeWidth={1.5} size={14} />
|
||||
) : ['isRestoring', 'isComingUp', 'isRestarting', 'isResizing'].includes(
|
||||
projectStatus ?? ''
|
||||
) ? (
|
||||
<RefreshCcw strokeWidth={1.5} size={14} />
|
||||
) : (
|
||||
<AlertTriangle strokeWidth={1.5} size={14} />
|
||||
)}
|
||||
</div>
|
||||
{/* Text and tooltip icon */}
|
||||
<div className="flex items-center w-full gap-x-2">
|
||||
<p className="text-xs">{alertTitle}</p>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info size={14} className="text-foreground-light hover:text-foreground" />
|
||||
<Info
|
||||
size={12}
|
||||
className="text-foreground-lighter hover:text-foreground transition-colors"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{alertDescription}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UIEvent, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { useDebounce } from '@uidotdev/usehooks'
|
||||
@@ -13,30 +13,13 @@ 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 { isAtBottom } from 'lib/helpers'
|
||||
import { parseAsArrayOf, parseAsString, useQueryState } from 'nuqs'
|
||||
import type { Organization } from 'types'
|
||||
import {
|
||||
Card,
|
||||
cn,
|
||||
LoadingLine,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import {
|
||||
LoadingCardView,
|
||||
LoadingTableRow,
|
||||
LoadingTableView,
|
||||
NoFilterResults,
|
||||
NoProjectsState,
|
||||
} from './EmptyStates'
|
||||
import { Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
import { LoadingCardView, LoadingTableView, NoProjectsState } from './EmptyStates'
|
||||
import { LoadMoreRows } from './LoadMoreRow'
|
||||
import { ProjectCard } from './ProjectCard'
|
||||
import { ProjectTableRow } from './ProjectTableRow'
|
||||
import { ShimmeringCard } from './ShimmeringCard'
|
||||
|
||||
export interface ProjectListProps {
|
||||
organization?: Organization
|
||||
@@ -65,7 +48,6 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
isLoading: isLoadingProjects,
|
||||
isSuccess: isSuccessProjects,
|
||||
isError: isErrorProjects,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
@@ -106,6 +88,8 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
const noResultsFromStatusFilter =
|
||||
filterStatus.length > 0 && isSuccessProjects && orgProjects.length === 0
|
||||
|
||||
const noResults = noResultsFromStatusFilter || noResultsFromSearch
|
||||
|
||||
const githubConnections = connections?.map((connection) => ({
|
||||
id: String(connection.id),
|
||||
added_by: {
|
||||
@@ -126,11 +110,6 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
?.filter((integration) => integration.integration.name === 'Vercel')
|
||||
.flatMap((integration) => integration.connections)
|
||||
|
||||
const handleScroll = (event: UIEvent<HTMLDivElement | HTMLUListElement>) => {
|
||||
if (isLoadingProjects || isFetchingNextPage || !isAtBottom(event)) return
|
||||
fetchNextPage()
|
||||
}
|
||||
|
||||
if (isErrorPermissions) {
|
||||
return (
|
||||
<AlertError
|
||||
@@ -159,38 +138,38 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<Card className="flex-1 min-h-0 overflow-y-auto mb-8" onScroll={handleScroll}>
|
||||
<Card className="flex-1 min-h-0 overflow-y-auto mb-8">
|
||||
<Table>
|
||||
{/* [Joshen] Ideally we can figure out sticky table headers here */}
|
||||
<TableHeader className="[&>tr>th]:sticky [&>tr>th]:top-0">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Project</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Compute</TableHead>
|
||||
<TableHead>Region</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
</TableRow>
|
||||
<TableRow className="!border-b-0">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<LoadingLine loading={isFetching} />
|
||||
</TableCell>
|
||||
<TableHead className={cn(noResults && 'text-foreground-muted')}>Project</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')}>Created</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{noResultsFromStatusFilter ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<NoFilterResults
|
||||
filterStatus={filterStatus}
|
||||
resetFilterStatus={() => setFilterStatus([])}
|
||||
className="border-0"
|
||||
<TableRow className="[&>td]:hover:bg-inherit">
|
||||
<TableCell colSpan={5}>
|
||||
<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>
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<NoSearchResults searchString={search} className="border-0" />
|
||||
<TableRow className="[&>td]:hover:bg-inherit">
|
||||
<TableCell colSpan={5}>
|
||||
<NoSearchResults searchString={search} withinTableCell />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -212,7 +191,13 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{hasNextPage && <LoadingTableRow />}
|
||||
{hasNextPage && (
|
||||
<LoadMoreRows
|
||||
type="table"
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
@@ -224,40 +209,52 @@ export const ProjectList = ({ organization: organization_, rewriteHref }: Projec
|
||||
return (
|
||||
<>
|
||||
{noResultsFromStatusFilter ? (
|
||||
<NoFilterResults
|
||||
filterStatus={filterStatus}
|
||||
resetFilterStatus={() => setFilterStatus([])}
|
||||
<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} />
|
||||
) : (
|
||||
<ul
|
||||
onScroll={handleScroll}
|
||||
className={cn(
|
||||
'min-h-0 w-full mx-auto overflow-y-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 pb-6'
|
||||
)}
|
||||
>
|
||||
{sortedProjects?.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.supabase_project_ref === project.ref
|
||||
)}
|
||||
vercelIntegration={vercelConnections?.find(
|
||||
(connection) => connection.supabase_project_ref === project.ref
|
||||
)}
|
||||
<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'
|
||||
)}
|
||||
>
|
||||
{sortedProjects?.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.supabase_project_ref === project.ref
|
||||
)}
|
||||
vercelIntegration={vercelConnections?.find(
|
||||
(connection) => connection.supabase_project_ref === project.ref
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{hasNextPage && (
|
||||
<LoadMoreRows
|
||||
type="card"
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
/>
|
||||
))}
|
||||
{hasNextPage && [...Array(2)].map((_, i) => <ShimmeringCard key={i} />)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -53,15 +53,17 @@ export const ProjectTableRow = ({
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{/* Text */}
|
||||
<div>
|
||||
<p className="font-medium">{name}</p>
|
||||
<p className="text-xs text-foreground-lighter">ID: {projectRef}</p>
|
||||
<h5 className="text-sm">{name}</h5>
|
||||
<p className="text-sm text-foreground-lighter">ID: {projectRef}</p>
|
||||
</div>
|
||||
{/* Integrations */}
|
||||
{(isGithubIntegrated || isVercelIntegrated) && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{isVercelIntegrated && (
|
||||
<div className="w-fit p-1 border rounded-md flex items-center text-black dark:text-white">
|
||||
<div className="bg-surface-100 w-5 h-5 p-1 border border-strong rounded-md flex items-center text-black dark:text-white">
|
||||
<InlineSVG
|
||||
src={`${BASE_PATH}/img/icons/vercel-icon.svg`}
|
||||
title="Vercel Icon"
|
||||
@@ -70,16 +72,14 @@ export const ProjectTableRow = ({
|
||||
</div>
|
||||
)}
|
||||
{isGithubIntegrated && (
|
||||
<>
|
||||
<div className="w-fit p-1 border rounded-md flex items-center">
|
||||
<div className="bg-surface-100 flex items-center gap-x-0.5 h-5 pr-1 border border-strong rounded-md">
|
||||
<div className="w-5 h-5 p-1 flex items-center">
|
||||
<Github size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
{githubRepository && (
|
||||
<span className="text-xs text-foreground-light truncate max-w-64">
|
||||
{githubRepository}
|
||||
</span>
|
||||
<p className="text-xs text-foreground-light truncate">{githubRepository}</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -102,7 +102,7 @@ export const ProjectTableRow = ({
|
||||
computeSize={getComputeSize(project)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-foreground-light">-</span>
|
||||
<span className="text-xs text-foreground-muted">–</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { useDebounce } from '@uidotdev/usehooks'
|
||||
import { Filter, Grid, List, Loader2, Plus, Search, X } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
ToggleGroupItem,
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { keepPreviousData } from '@tanstack/react-query'
|
||||
|
||||
interface HomePageActionsProps {
|
||||
slug?: string
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ComputeBadgeWrapper = ({
|
||||
return (
|
||||
<HoverCard onOpenChange={() => setOpenState(!open)} openDelay={280}>
|
||||
<HoverCardTrigger asChild className="group" onClick={(e) => e.stopPropagation()}>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<ComputeBadge infraComputeSize={computeSize} />
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
export interface NoSearchResultsProps {
|
||||
searchString: string
|
||||
searchString?: string
|
||||
withinTableCell?: boolean
|
||||
onResetFilter?: () => void
|
||||
className?: string
|
||||
label?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const NoSearchResults = ({
|
||||
searchString,
|
||||
withinTableCell = false,
|
||||
onResetFilter,
|
||||
className,
|
||||
label,
|
||||
description,
|
||||
}: NoSearchResultsProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-surface-100 border border-default px-6 py-4 rounded flex items-center justify-between',
|
||||
'flex items-center justify-between',
|
||||
!withinTableCell && 'bg-surface-100 px-4 md:px-6 py-4 rounded-md border border-default',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{searchString}" did not return any results
|
||||
<div className="text-sm flex flex-col gap-y-0.5">
|
||||
<p className="text-foreground">{label ?? 'No results found'}</p>
|
||||
<p className="text-foreground-lighter">
|
||||
{description ?? `Your search for “${searchString}” did not return any results`}
|
||||
</p>
|
||||
</div>
|
||||
{onResetFilter !== undefined && (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useIsMFAEnabled } from 'common'
|
||||
import { ProjectList } from 'components/interfaces/Home/ProjectList/ProjectList'
|
||||
import { HomePageActions } from 'components/interfaces/HomePageActions'
|
||||
@@ -5,9 +7,9 @@ import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import OrganizationLayout from 'components/layouts/OrganizationLayout'
|
||||
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => {
|
||||
@@ -20,17 +22,24 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
<ScaffoldContainer className="flex-grow flex">
|
||||
<ScaffoldSection isFullWidth className="flex-grow pb-0">
|
||||
{disableAccessMfa ? (
|
||||
<Admonition type="note" title={`The organization "${org?.name}" has MFA enforced`}>
|
||||
<p className="!m-0">
|
||||
Set up MFA on your account through your{' '}
|
||||
<InlineLink href="/account/security">account preferences</InlineLink> to access this
|
||||
organization
|
||||
</p>
|
||||
</Admonition>
|
||||
<Admonition
|
||||
type="note"
|
||||
layout="horizontal"
|
||||
title={`${org?.name} requires MFA`}
|
||||
description={
|
||||
<>
|
||||
Set up multi-factor authentication (MFA) on your account to access this
|
||||
organization’s projects.
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<Button asChild type="default">
|
||||
<Link href="/account/security">Set up MFA</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// [Joshen] Very odd, but the h-px here is required for ProjectList to have a max
|
||||
// height based on the remaining space that it can grow to
|
||||
<div className="flex flex-col gap-y-4 flex-grow h-px">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<HomePageActions showViewToggle={true} />
|
||||
<ProjectList />
|
||||
</div>
|
||||
|
||||
@@ -28,12 +28,16 @@ export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant = 'default', children, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Forward refs in order to allow tooltips to be applied to the badge
|
||||
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ className, variant = 'default', children, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={cn(badgeVariants({ variant }), className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Badge.displayName = 'Badge'
|
||||
|
||||
export { Badge }
|
||||
|
||||
@@ -70,6 +70,8 @@ const TableHead = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-10 px-4 text-left align-middle heading-meta whitespace-nowrap text-foreground-lighter [&:has([role=checkbox])]:pr-0',
|
||||
// Transition text color when NoSearchResults or NoFilterResults empty state is shown
|
||||
'transition-colors',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user