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:
Danny White
2025-12-12 19:35:43 +10:00
committed by GitHub
parent 28e4473953
commit 73ece8a470
13 changed files with 286 additions and 229 deletions

View File

@@ -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 projects registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration.

View File

@@ -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"

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>
)}
</>
)

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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
organizations 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>

View File

@@ -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 }

View File

@@ -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}