Remove home page skeleton loader, improve base loading behaviour (#47903)

## Context

The project's home page has been updated for a while now, but the
skeleton loader is still showing the old layout

Am opting to remove the skeleton loader entirely and instead improve the
loading state of the base UI - adjusts some heights to minimize layout
shifts

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Added shimmer loading for project titles while project details are
loading.
* Refined project connection dropdown behavior: improved URL
display/truncation, adjusted dropdown alignment, and more reliable
option selection (disabled items remain unavailable).
* Streamlined the project page’s loading and transition experience
across different project states.
* **Tests**
* Updated the connect E2E scenario to interact with the Connect sheet
via its accessible label before selecting the ORM option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-07-14 23:55:23 +08:00
1 parent 8b82d5c472
commit 88f3173920
5 files changed
+85 -132

No files matched your search

@@ -164,84 +164,89 @@ export const ProjectConnectionPopover = ({ projectRef }: ProjectConnectionPopove
}, [open])
return (
<div className="mt-3 inline-flex max-w-full items-center gap-3 min-w-0">
<div className="mt-3 flex items-center gap-3">
{isLoadingApiUrl ? (
<ShimmeringLoader className="w-32 shrink-0" />
<ShimmeringLoader className="w-80" />
) : (
<span className="min-w-0 max-w-[320px] truncate text-left text-foreground-light">
<span className="min-w-0 max-w-[400px] truncate text-left text-foreground-light">
{projectUrl ?? 'Project URL unavailable'}
</span>
)}
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="default"
size="tiny"
className="shrink-0"
iconRight={
<ChevronDown size={14} className={cn('transition-transform', open && 'rotate-180')} />
}
>
Copy <span className="sr-only">project URL and API keys</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-80 p-1">
{menuItems.map((item) => {
const Icon = item.icon
return (
<DropdownMenuItem
key={item.label}
className="group relative items-center gap-3 pr-10"
disabled={item.disabled}
onSelect={(event) => {
event.preventDefault()
if (item.disabled) return
{!isLoadingApiUrl && (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
size="tiny"
variant="default"
iconRight={
<ChevronDown
size={14}
className={cn('transition-transform', open && 'rotate-180')}
/>
}
>
Copy <span className="sr-only">project URL and API keys</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center" className="w-80 p-1">
{menuItems.map((item) => {
const Icon = item.icon
copyToClipboard(item.value)
setCopiedItem(item.label)
return (
<DropdownMenuItem
key={item.label}
className="group relative items-center gap-3 pr-10"
disabled={item.disabled}
onSelect={(event) => {
event.preventDefault()
if (item.disabled) return
copyToClipboard(item.value)
setCopiedItem(item.label)
}}
>
<Icon size={14} className="mt-0.5 shrink-0 text-foreground-light" />
<div className="min-w-0 flex-1">
<div className="text-sm text-foreground">
{copiedItem !== item.label ? <span className="sr-only">Copy</span> : null}
{item.label}
{copiedItem === item.label ? (
<span className="sr-only">copied to your clipboard</span>
) : null}
</div>
<div className="truncate text-sm text-foreground-lighter">
{item.displayValue}
</div>
</div>
<div
className={cn(
'absolute right-2 top-1/2 -translate-y-1/2 text-foreground-lighter opacity-0 transition-opacity group-hover:opacity-100',
copiedItem === item.label && 'opacity-100 text-brand'
)}
>
{copiedItem === item.label ? <Check size={14} /> : <Copy size={14} />}
</div>
</DropdownMenuItem>
)
})}
<DropdownMenuSeparator />
<div className="p-1">
<Button
variant="default"
size="tiny"
className="w-full"
onClick={() => {
setOpen(false)
setShowConnect(true)
}}
>
<Icon size={14} className="mt-0.5 shrink-0 text-foreground-light" />
<div className="min-w-0 flex-1">
<div className="text-sm text-foreground">
{copiedItem !== item.label ? <span className="sr-only">Copy</span> : null}
{item.label}
{copiedItem === item.label ? (
<span className="sr-only">copied to your clipboard</span>
) : null}
</div>
<div className="truncate text-sm text-foreground-lighter">
{item.displayValue}
</div>
</div>
<div
className={cn(
'absolute right-2 top-1/2 -translate-y-1/2 text-foreground-lighter opacity-0 transition-opacity group-hover:opacity-100',
copiedItem === item.label && 'opacity-100 text-brand'
)}
>
{copiedItem === item.label ? <Check size={14} /> : <Copy size={14} />}
</div>
</DropdownMenuItem>
)
})}
<DropdownMenuSeparator />
<div className="p-1">
<Button
variant="default"
size="tiny"
className="w-full"
onClick={() => {
setOpen(false)
setShowConnect(true)
}}
>
Get Connected
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
Get Connected
</Button>
</div>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
)
}
@@ -1,6 +1,7 @@
import { ReactFlowProvider } from '@xyflow/react'
import Link from 'next/link'
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { InstanceConfiguration } from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
import { ActivityStats } from '@/components/interfaces/ProjectHome/ActivityStats'
@@ -15,7 +16,7 @@ import { DOCS_URL, IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
export const TopSection = () => {
const isOrioleDb = useIsOrioleDb()
const { data: project } = useSelectedProjectQuery()
const { data: project, isLoading } = useSelectedProjectQuery()
const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref })
const { data: branches } = useBranchesQuery({
@@ -58,7 +59,11 @@ export const TopSection = () => {
</Link>
)}
<div className="flex items-center gap-x-2">
<h1 className="text-3xl">{projectName}</h1>
{isLoading ? (
<ShimmeringLoader className="w-32 py-0 h-[33.6px]" />
) : (
<h1 className="text-3xl">{projectName}</h1>
)}
{isOrioleDb && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1,50 +0,0 @@
import { useParams } from 'common'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
export const LoadingState = () => {
const { ref } = useParams()
const { data: project, isPending: isLoading } = useProjectDetailQuery({ ref })
const projectName = ref !== 'default' ? project?.name : 'Welcome to your project'
return (
<div className="w-full mx-auto">
<div className="px-8 border-b">
<div className="max-w-7xl mx-auto flex items-center space-x-6 h-[184px]">
{isLoading ? (
<ShimmeringLoader className="h-9 w-40" />
) : (
<h1 className="text-3xl">{projectName}</h1>
)}
</div>
</div>
<div className="px-8 py-16">
<div className="max-w-7xl mx-auto">
<ProjectUsageLoadingState />
</div>
</div>
<div className="px-8">
<div className="max-w-7xl mx-auto space-y-4">
<ShimmeringLoader className="w-40 h-7" />
<ShimmeringLoader className="w-full h-32" />
</div>
</div>
</div>
)
}
export const ProjectUsageLoadingState = () => {
return (
<div className="space-y-6">
<ShimmeringLoader className="w-40 h-7" />
<div className="flex flex-col md:flex-row gap-2 md:gap-4">
<ShimmeringLoader className="w-full h-[320px] py-0" />
<ShimmeringLoader className="w-full h-[320px] py-0" />
<ShimmeringLoader className="w-full h-[320px] py-0" />
<ShimmeringLoader className="w-full h-[320px] py-0" />
</div>
</div>
)
}
@@ -31,7 +31,6 @@ import ProductMenuBar from '../Navigation/ProductMenuBar'
import BuildingState from './BuildingState'
import ConnectingState from './ConnectingState'
import { getSectionKeyFromPathname, MobileMenuContent } from './LayoutHeader/MobileMenuContent'
import { LoadingState } from './LoadingState'
import { ProjectPausedState } from './PausedState/ProjectPausedState'
import { PauseFailedState } from './PauseFailedState'
import { PausingState } from './PausingState'
@@ -355,7 +354,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
</div>
</div>
) : (
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
<ContentWrapper>
<ResourceExhaustionWarningBanner />
{children}
</ContentWrapper>
@@ -403,8 +402,6 @@ const MenuBarWrapper = ({
}
interface ContentWrapperProps {
isLoading: boolean
isBlocking?: boolean
children: ReactNode
}
@@ -420,7 +417,7 @@ interface ContentWrapperProps {
*
* [TODO] Next iteration should scrape long polling and just listen to the project's status
*/
const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapperProps) => {
const ContentWrapper = ({ children }: ContentWrapperProps) => {
const router = useRouter()
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
@@ -430,7 +427,6 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
const requiresDbConnection = !routesToIgnoreDBConnection.some((x) => router.pathname.includes(x))
const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname)
const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname)
const isRestarting = selectedProject?.status === PROJECT_STATUS.RESTARTING
const isResizing = selectedProject?.status === PROJECT_STATUS.RESIZING
@@ -465,10 +461,6 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
if (ref) state.setSelectedDatabaseId(ref)
}, [ref])
if (isBlocking && (isLoading || (requiresProjectDetails && selectedProject === undefined))) {
return router.pathname.endsWith('[ref]') ? <LoadingState /> : <LogoLoader />
}
if (isRestarting && !isBackupsPage) {
return <RestartingState />
}
+2 -1
View File
@@ -168,7 +168,8 @@ test.describe('Connect Sheet deep linking', async () => {
await expect(page, 'framework param should be in URL initially').toHaveURL(/framework=nextjs/)
await page.getByRole('button', { name: /ORM/ }).click()
const connectSheet = page.getByLabel('Connect to your project')
await connectSheet.getByRole('button', { name: /ORM/ }).click()
await expect(page, 'framework param should be cleared after mode change').not.toHaveURL(
/framework=nextjs/