mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
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:
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 />
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in new issue
Block a user