This commit is contained in:
Jonathan Summers-Muir committed 2024-12-17 23:26:14 +08:00
1 parent 50ed9fb89c
commit 80abc1b5da
14 files changed
+542 -243

No files matched your search

@@ -200,30 +200,32 @@ export function AppDefaultNavigation() {
function NavLink({ route, active }: { route: any; active?: boolean }) {
const router = useRouter()
const hasItems = route.items && route.items.some((section) => section.items.length > 0)
// const hasItems = route.items && route.items.some((section) => section.items.length > 0)
const [open, setOpen] = useState(false)
// if (!hasItems) {
return (
<SidebarMenuItem key={route.key}>
<SidebarMenuButton
asChild
isActive={active}
className="text-sm [&_svg]:opacity-50"
size={'sm'}
>
<Link href={route.link ?? ''}>
{route.icon}
{route.label}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
// }
// console.log('route', route)
if (!route.items) {
return (
<SidebarMenuItem key={route.key}>
<SidebarMenuButton
asChild
isActive={active}
className="text-sm [&_svg]:opacity-50"
size={'sm'}
>
<Link href={route.link ?? ''}>
{route.icon}
{route.label}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
}
return (
<Collapsible_Shadcn_ className="group/collapsible" open={open} onOpenChange={setOpen}>
<SidebarMenuItem key={route.key}>
<SidebarMenuItem>
<CollapsibleTrigger_Shadcn_ asChild>
<SidebarMenuButton
className="text-sm flex items-center [&_svg]:opacity-50 data-[active=true]>[&>svg]:opacity-100"
@@ -247,25 +249,16 @@ function NavLink({ route, active }: { route: any; active?: boolean }) {
style={{ overflow: 'hidden' }}
>
<SidebarMenuSub className="gap-0.5">
{route.items.map((item) =>
item.items
.filter((x) => {
if (!x.rightIcon) {
return x
}
})
.map((subItem) => (
<SidebarMenuSubItem key={subItem.key}>
<SidebarMenuSubButton
asChild
isActive={router.asPath === subItem.url}
size="sm"
>
<Link href={subItem.url ?? ''}>{subItem.name}</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))
)}
{route.items.map((item, i) => {
// console.log('item', item)
return (
<SidebarMenuSubItem key={`${item.key}-${i}`}>
<SidebarMenuSubButton asChild isActive={router.asPath === item.link} size="md">
<Link href={item.link ?? '/wrong-url'}>{item.title}</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
)
})}
</SidebarMenuSub>
</motion.div>
</CollapsibleContent_Shadcn_>
@@ -303,11 +296,12 @@ function ProjectLinks() {
const otherRoutes = generateOtherRoutes(ref, project)
const settingsRoutes = generateSettingsRoutes(ref, project)
console.log(productRoutes)
// console.log(productRoutes)
return (
<SidebarMenu className="gap-[2px]">
<NavLink
key="home"
active={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
route={{
key: 'HOME',
@@ -317,25 +311,25 @@ function ProjectLinks() {
linkElement: <ProjectIndexPageLink projectRef={ref} />,
}}
/>
{toolRoutes.map((route) => (
<NavLink key={route.key} route={route} active={activeRoute === route.key} />
{toolRoutes.map((route, i) => (
<NavLink key={`tools-routes-${i}`} route={route} active={activeRoute === route.key} />
))}
<Separator className="my-1 bg-border-muted" />
<span className="text-foreground-muted text-xs uppercase font-mono text-[12px] px-1.5 mt-2 tracking-wider">
products
</span>
{productRoutes.map((route) => (
<NavLink key={route.key} route={route} active={activeRoute === route.key} />
{productRoutes.map((route, i) => (
<NavLink key={`product-routes-${i}`} route={route} active={activeRoute === route.key} />
))}
<Separator className="my-1 bg-border-muted" />
<span className="text-foreground-muted text-xs uppercase font-mono text-[12px] px-1.5 mt-2 tracking-wider">
develop
</span>
{otherRoutes.map((route) => (
<NavLink key={route.key} route={route} active={activeRoute === route.key} />
{otherRoutes.map((route, i) => (
<NavLink key={`other-routes-${i}`} route={route} active={activeRoute === route.key} />
))}
{settingsRoutes.map((route) => (
<NavLink key={route.key} route={route} active={activeRoute === route.key} />
{settingsRoutes.map((route, i) => (
<NavLink key={`settings-routes-${i}`} route={route} active={activeRoute === route.key} />
))}
</SidebarMenu>
)
@@ -59,8 +59,7 @@ const AuthLayout = ({ title, children }: PropsWithChildren<AuthLayoutProps>) =>
// productMenu={<AuthProductMenu />}
isBlocking={false}
>
<ProjectPageNavigation items={generateAuthMenu(ref as string)} />
{children}
<ProjectPageNavigation navKey="auth">{children}</ProjectPageNavigation>
</ProjectLayout>
)
}
@@ -1,15 +1,25 @@
import { useParams } from 'common'
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import type {
ProductMenuGroup,
ProductMenuGroupItem,
} from 'components/ui/ProductMenu/ProductMenu.types'
import { IS_PLATFORM } from 'lib/constants'
export const generateAuthMenu = (ref: string): ProductMenuGroup[] => {
export const generateAuthMenu = (ref: string | undefined): ProductMenuGroup[] => {
if (!ref) return []
return [
{
title: 'Manage',
title: 'Users',
name: 'Users',
key: 'users',
link: `/project/${ref}/auth/users`,
items: [{ name: 'Users', key: 'users', url: `/project/${ref}/auth/users`, items: [] }],
},
{
title: 'Configuration',
name: 'Configuration',
key: 'configuration',
link: `/project/${ref}/auth/configuration`,
items: [
// {
// name: 'Policies',
@@ -64,78 +74,65 @@ export const generateAuthMenu = (ref: string): ProductMenuGroup[] => {
]
}
export const generateAuthConfigurationMenu = ({ ref }: { ref: string }): ProductMenuGroup[] => {
export const generateAuthPageMenu = (ref: string): ProductMenuGroupItem[] => {
return [
{
title: 'Configuration',
items: [
...(IS_PLATFORM
? [
{
name: 'Sign-in method',
key: 'Sign-in method',
url: `/project/${ref}/auth/providers`,
items: [],
},
{
name: 'Session',
key: 'session',
url: `/project/${ref}/auth/session`,
items: [],
},
{
name: 'MFA',
key: 'mfa',
url: `/project/${ref}/auth/mfa`,
items: [],
},
{
name: 'Third party',
key: 'third-party',
url: `/project/${ref}/auth/third-party`,
items: [],
},
{
name: 'Social login',
key: 'providers',
url: `/project/${ref}/auth/social-login`,
items: [],
},
{
name: 'Rate Limits',
key: 'rate-limits',
url: `/project/${ref}/auth/rate-limits`,
items: [],
},
{
name: 'Email Templates',
key: 'templates',
url: `/project/${ref}/auth/templates`,
items: [],
},
{
name: 'URL Configuration',
key: 'url-configuration',
url: `/project/${ref}/auth/url-configuration`,
items: [],
},
{
name: 'Hooks',
key: 'hooks',
url: `/project/${ref}/auth/hooks`,
items: [],
label: 'BETA',
},
{
name: 'Configuration',
key: 'configuration',
url: `/project/${ref}/auth/configuration`,
items: [],
label: 'BETA',
},
]
: []),
],
name: 'Users',
key: 'users',
url: `/project/${ref}/auth/users`,
},
{
name: 'Sign-in method',
key: 'Sign-in method',
url: `/project/${ref}/auth/providers`,
},
{
name: 'Session',
key: 'session',
url: `/project/${ref}/auth/session`,
},
{
name: 'MFA',
key: 'mfa',
url: `/project/${ref}/auth/mfa`,
},
{
name: 'Third party',
key: 'third-party',
url: `/project/${ref}/auth/third-party`,
},
{
name: 'Rate Limits',
key: 'rate-limits',
url: `/project/${ref}/auth/rate-limits`,
},
{
name: 'Emails',
key: 'templates',
url: `/project/${ref}/auth/templates`,
},
// {
// name: 'Email Templates',
// key: 'templates',
// url: `/project/${ref}/auth/templates`,
// },
// {
// name: 'URL Configuration',
// key: 'url-configuration',
// url: `/project/${ref}/auth/url-configuration`,
// },
{
name: 'Hooks',
key: 'hooks',
url: `/project/${ref}/auth/hooks`,
label: 'BETA',
},
{
name: 'Configuration',
key: 'configuration',
url: `/project/${ref}/auth/configuration`,
label: 'BETA',
},
]
}
@@ -1,34 +1,89 @@
'use client'
import { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { ProductMenuGroup, ProductMenuGroupItem } from 'components/ui/ProductMenu/ProductMenu.types'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { PropsWithChildren, ReactNode } from 'react'
import { NavMenu, NavMenuItem } from 'ui'
import {
generateDatabaseMenu,
generatePostgresItemsMenu,
} from '../DatabaseLayout/DatabaseMenu.utils'
import { generateAuthMenu, generateAuthPageMenu } from './AuthLayout.utils'
import { useParams } from 'common'
import { generateStoragePageItemsMenu } from '../StorageLayout/StorageLayout.utils'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
export function ProjectPageNavigation({ items }: { items: ProductMenuGroup[] }) {
export function ProjectPageNavigation({ children, navKey }: PropsWithChildren<{ navKey: string }>) {
const params = useParams()
const pathname = usePathname()
const ref = params.ref as string
let items: ProductMenuGroupItem[] = []
const postgresItemsMenu = generatePostgresItemsMenu(ref as string)
const authMenu = generateAuthPageMenu(ref as string)
const storagePageItemsMenu = generateStoragePageItemsMenu(ref as string)
switch (navKey) {
case 'auth':
items = authMenu
break
case 'postgres-items':
items = postgresItemsMenu
break
case 'storage':
items = storagePageItemsMenu
break
default:
items = []
}
return (
<NavMenu className="px-5 pt-2 bg-dash-sidebar">
{items.map((item) => {
return (
<>
{/* <span className="text-foreground-muted">{item.title}</span> */}
{item.items &&
item.items.map((item) => {
return (
<>
<NavMenu className="px-5 pt-1 bg-dash-sidebar [&_ul]:gap-4">
{items.map((item) => {
const childIsActive = params[item.childId] && pathname?.includes(item.url)
return (
<>
<Link href={item.url}>
<NavMenuItem
active={!childIsActive && pathname?.includes(item.url)}
className="py-2 text-sm"
>
{item.name}
</NavMenuItem>
</Link>
<AnimatePresence>
{item.hasChild && params[item.childId] && (
<>
<Link href={item.url}>
<NavMenuItem active={pathname?.includes(item.url)} className="py-2">
{item.name}
<motion.div
initial={{ opacity: 0, x: -10, width: 0 }}
animate={{ opacity: 1, x: 0, width: 'auto' }}
exit={{ opacity: 0, x: -10, width: 0 }}
transition={{ duration: 0.12, delay: 0.05 }}
className="flex items-center -ml-2"
>
<ChevronRight
size={14}
className="text-foreground-muted mr-2 flex-shrink-0"
/>
<NavMenuItem active={true} className="flex items-center gap-2">
{item.childIcon}
<Link href={`${item.url}/${params.childId}`}>{params[item.childId]}</Link>
</NavMenuItem>
</Link>
</motion.div>
</>
)
})}
</>
)
})}
</NavMenu>
)}
</AnimatePresence>
</>
)
})}
</NavMenu>
{children}
</>
)
}
@@ -9,42 +9,45 @@ import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { withAuth } from 'hooks/misc/withAuth'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { generateDatabaseMenu } from './DatabaseMenu.utils'
import { ProjectPageNavigation } from '../AuthLayout/ProjectPageNavigation'
import { useParams } from 'common'
export interface DatabaseLayoutProps {
title?: string
}
const DatabaseProductMenu = () => {
const project = useSelectedProject()
// const DatabaseProductMenu = () => {
// const project = useSelectedProject()
const router = useRouter()
const page = router.pathname.split('/')[4]
// const router = useRouter()
// const page = router.pathname.split('/')[4]
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
// const { data } = useDatabaseExtensionsQuery({
// projectRef: project?.ref,
// connectionString: project?.connectionString,
// })
// const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
// const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
// const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
// const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
return (
<>
<ProductMenu
page={page}
menu={generateDatabaseMenu(project, {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
})}
/>
</>
)
}
// return (
// <>
// <ProductMenu
// page={page}
// menu={generateDatabaseMenu(project.ref as string, {
// pgNetExtensionExists,
// pitrEnabled,
// columnLevelPrivileges,
// })}
// />
// </>
// )
// }
const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) => {
const { ref } = useParams()
return (
<ProjectLayout
product="Database"
@@ -1,10 +1,14 @@
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { useParams } from 'common'
import type {
ProductMenuGroup,
ProductMenuGroupItem,
} from 'components/ui/ProductMenu/ProductMenu.types'
import type { Project } from 'data/projects/project-detail-query'
import { IS_PLATFORM } from 'lib/constants'
import { ArrowUpRight } from 'lucide-react'
export const generateDatabaseMenu = (
ref?: string,
ref: string,
flags?: {
pgNetExtensionExists: boolean
pitrEnabled: boolean
@@ -12,10 +16,12 @@ export const generateDatabaseMenu = (
}
): ProductMenuGroup[] => {
const { pgNetExtensionExists, pitrEnabled, columnLevelPrivileges } = flags || {}
return [
{
title: 'Database Management',
title: 'Postgres Items',
name: 'Postgres Items',
key: 'postgres-items',
link: `/project/${ref}/database/tables`,
items: [
{
name: 'Schema Visualizer',
@@ -61,47 +67,6 @@ export const generateDatabaseMenu = (
url: `/project/${ref}/database/publications`,
items: [],
},
],
},
{
title: 'Access Control',
items: [
{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] },
...(columnLevelPrivileges
? [
{
name: 'Column Privileges',
key: 'column-privileges',
url: `/project/${ref}/database/column-privileges`,
items: [],
label: 'ALPHA',
},
]
: []),
{
name: 'Policies',
key: 'policies',
url: `/project/${ref}/auth/policies`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
],
},
{
title: 'Platform',
items: [
...(IS_PLATFORM
? [
{
name: 'Backups',
key: 'backups',
url: pitrEnabled
? `/project/${ref}/database/backups/pitr`
: `/project/${ref}/database/backups/scheduled`,
items: [],
},
]
: []),
{
name: 'Migrations',
key: 'migrations',
@@ -129,30 +94,151 @@ export const generateDatabaseMenu = (
],
},
{
title: 'Tools',
title: 'Access Control',
name: 'Access Control',
key: 'access-control',
link: `/project/${ref}/database/policies`,
items: [
{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] },
...(columnLevelPrivileges
? [
{
name: 'Column Privileges',
key: 'column-privileges',
url: `/project/${ref}/database/column-privileges`,
items: [],
label: 'ALPHA',
},
]
: []),
{
name: 'Security Advisor',
key: 'security-advisor',
url: `/project/${ref}/advisors/security`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
{
name: 'Performance Advisor',
key: 'performance-advisor',
url: `/project/${ref}/advisors/performance`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/advisors/query-performance`,
name: 'Policies',
key: 'policies',
url: `/project/${ref}/auth/policies`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
],
},
{
title: 'Backups',
name: 'Backups',
key: 'platform',
link: `/project/${ref}/database/backups`,
items: [
...(IS_PLATFORM
? [
{
name: 'Backups',
key: 'backups',
url: pitrEnabled
? `/project/${ref}/database/backups/pitr`
: `/project/${ref}/database/backups/scheduled`,
items: [],
},
]
: []),
],
},
// {
// title: 'Tools',
// name: 'Tools',
// key: 'tools',
// link: `/project/${ref}/advisors/security`,
// items: [
// {
// name: 'Security Advisor',
// key: 'security-advisor',
// url: `/project/${ref}/advisors/security`,
// rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
// items: [],
// },
// {
// name: 'Performance Advisor',
// key: 'performance-advisor',
// url: `/project/${ref}/advisors/performance`,
// rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
// items: [],
// },
// {
// name: 'Query Performance',
// key: 'query-performance',
// url: `/project/${ref}/advisors/query-performance`,
// rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
// items: [],
// },
// ],
// },
]
}
export const generatePostgresItemsMenu = (
ref: string,
flags?: {
pgNetExtensionExists: boolean
pitrEnabled: boolean
columnLevelPrivileges: boolean
}
): ProductMenuGroupItem[] => {
const { pgNetExtensionExists, pitrEnabled, columnLevelPrivileges } = flags || {}
return [
{
name: 'Schema Visualizer',
key: 'schemas',
url: `/project/${ref}/database/schemas`,
},
{ name: 'Tables', key: 'tables', url: `/project/${ref}/database/tables`, items: [] },
{
name: 'Functions',
key: 'functions',
url: `/project/${ref}/database/functions`,
},
{
name: 'Triggers',
key: 'triggers',
url: `/project/${ref}/database/triggers`,
},
{
name: 'Enumerated Types',
key: 'types',
url: `/project/${ref}/database/types`,
},
{
name: 'Extensions',
key: 'extensions',
url: `/project/${ref}/database/extensions`,
},
{
name: 'Indexes',
key: 'indexes',
url: `/project/${ref}/database/indexes`,
},
{
name: 'Publications',
key: 'publications',
url: `/project/${ref}/database/publications`,
},
{
name: 'Migrations',
key: 'migrations',
url: `/project/${ref}/database/migrations`,
},
{
name: 'Wrappers',
key: 'wrappers',
url: `/project/${ref}/integrations?category=wrapper`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
},
// ...(!!pgNetExtensionExists
// ? [
{
name: 'Webhooks',
key: 'hooks',
url: `/project/${ref}/integrations/webhooks/overview`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
// ]
// : []),
]
}
@@ -54,19 +54,24 @@ export const generateProductRoutes = (
const storageEnabled = features?.storage ?? true
const realtimeEnabled = features?.realtime ?? true
const databaseMenu = generateDatabaseMenu(ref as string)
const authMenu = generateAuthMenu(ref as string)
return [
{
key: 'database',
label: 'Database',
icon: <Database size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link:
ref &&
(isProjectBuilding
? buildingUrl
: isProjectActive
? `/project/${ref}/database/schemas`
: `/project/${ref}/database/backups/scheduled`),
items: generateDatabaseMenu(ref as string),
// ref
// &&
// (isProjectBuilding
// ? buildingUrl
// : isProjectActive
// ?
`/project/${ref}/database/schemas`,
// : `/project/${ref}/database/backups/scheduled`),
items: databaseMenu,
},
...(authEnabled
? [
@@ -75,7 +80,7 @@ export const generateProductRoutes = (
label: 'Authentication',
icon: <Auth size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/auth/users`),
items: generateAuthMenu(ref as string),
// items: authMenu,
},
]
: []),
@@ -9,6 +9,7 @@ import { PROJECT_STATUS } from 'lib/constants'
import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import StorageMenu from './StorageMenu'
import { ProjectPageNavigation } from '../AuthLayout/ProjectPageNavigation'
export interface StorageLayoutProps {
title: string
@@ -48,7 +49,7 @@ const StorageLayout = ({ title, children }: StorageLayoutProps) => {
return (
<ProjectLayout title={title || 'Storage'} product="Storage" productMenu={<StorageMenu />}>
{children}
<ProjectPageNavigation navKey="storage">{children}</ProjectPageNavigation>
</ProjectLayout>
)
}
@@ -0,0 +1,39 @@
import type { ProductMenuGroupItem } from 'components/ui/ProductMenu/ProductMenu.types'
import { Archive } from 'lucide-react'
export const generateStoragePageItemsMenu = (
ref: string,
flags?: {
pgNetExtensionExists: boolean
pitrEnabled: boolean
columnLevelPrivileges: boolean
}
): ProductMenuGroupItem[] => {
return [
{
name: 'Buckets',
key: 'buckets',
url: `/project/${ref}/storage/buckets`,
hasChild: true,
childId: 'bucketId',
childIcon: (
<Archive size={12} strokeWidth={1.5} className={'text-foreground w-full h-full'} />
),
},
{
name: 'Image transformations',
key: 'image-transformations',
url: `/project/${ref}/storage/image-transformations`,
},
{
name: 'Uploads',
key: 'uploads',
url: `/project/${ref}/storage/uploads`,
},
{
name: 'S3',
key: 's3',
url: `/project/${ref}/storage/s3`,
},
]
}
@@ -0,0 +1,110 @@
import { AnimatePresence, motion, MotionProps, useScroll, useTransform } from 'framer-motion'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { ComponentProps, ComponentType, useRef } from 'react'
import { useParams } from 'common'
import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants'
import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { NavMenu, NavMenuItem } from 'ui'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
const MotionNavMenu = motion(NavMenu) as ComponentType<ComponentProps<typeof NavMenu> & MotionProps>
// Output range: The padding range for the nav (from compact to expanded)
const paddingRange = [40, 86]
// Output range: The padding range for the image
const iconPaddingRange = [3, 1.5] // From 1.5px (scrolled) to 4px (top)
interface IntegrationTabsProps {
scroll: ReturnType<typeof useScroll>
isSticky?: boolean
}
export const IntegrationTabs = ({ tabs, scroll, isSticky }: IntegrationTabsProps) => {
const navRef = useRef(null)
const { project } = useProjectContext()
const { id, pageId, childId } = useParams()
const { installedIntegrations } = useInstalledIntegrations()
// Find the integration details based on ID
const integration = INTEGRATIONS.find((i) => i.id === id)
const headerRef = useRef<HTMLDivElement>(null)
// Input range: The scrollY range for triggering the animation (e.g., 0 to 200px of scroll)
const scrollRange = [40, headerRef.current?.offsetHeight ?? 128]
const navInnerLeftPaddingX = useTransform(scroll?.scrollY!, scrollRange, paddingRange)
const iconPadding = useTransform(scroll?.scrollY!, scrollRange, iconPaddingRange)
const installedIntegration = installedIntegrations?.find((i) => i.id === id)
if (!integration) return null
return (
<AnimatePresence>
<div className="sticky top-[0px] z-[1] bg-dash-sidebar" ref={navRef}>
<MotionNavMenu
transition={{ duration: 0.2 }}
className="px-10 [&_ul]:items-center bg-200"
aria-label="Integration menu"
style={{ paddingLeft: !isSticky ? (navInnerLeftPaddingX as unknown as number) : 40 }}
>
{isSticky && (
<motion.div
layoutId="integration-icon"
className="w-[20px] h-[20px] relative bg-white rounded"
transition={{ duration: 0 }}
>
{integration?.icon({
style: { padding: iconPadding.get() },
})}
</motion.div>
)}
{tabs.map((tab) => {
const tabUrl = `/project/${project?.ref}/integrations/${integration?.id}/${tab.route}`
return (
<div className="flex items-center gap-2" key={tab.route}>
<NavMenuItem active={pageId === tab.route && !childId}>
<Link href={tabUrl}>{tab.label}</Link>
</NavMenuItem>
<AnimatePresence>
{tab.hasChild && childId && (
<>
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.12, delay: 0.05 }}
className="flex items-center"
>
<ChevronRight size={14} className="text-foreground-muted" />
</motion.div>
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: 0.12, delay: 0.1 }}
className="flex items-center"
>
<NavMenuItem active={true} className="flex items-center gap-2">
{tab.childIcon}
<Link href={`${tabUrl}/${childId}`}>{childId}</Link>
</NavMenuItem>
</motion.div>
</>
)}
</AnimatePresence>
</div>
)
})}
</MotionNavMenu>
</div>
</AnimatePresence>
)
}
IntegrationTabs.displayName = 'IntegrationTabs'
@@ -184,7 +184,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => {
className="
bg-studio
border-overlay flex
h-full w-full flex-col rounded-md border"
h-full w-full flex-col"
>
{selectedItems.length === 0 ? (
<FileExplorerHeader
@@ -4,17 +4,23 @@ export interface ProductMenuGroup {
title?: string
key?: string
isPreview?: boolean
name?: string
items: ProductMenuGroupItem[]
link?: string
}
export interface ProductMenuGroupItem {
name: string
key: string
url: string
items: any[]
items?: any[]
icon?: ReactNode
rightIcon?: ReactNode
isExternal?: boolean
disabled?: boolean
label?: string
hasChild?: boolean
childId?: string
childIcon?: ReactNode
}
@@ -11,6 +11,8 @@ import { useTableEditorStateSnapshot } from 'state/table-editor'
import type { NextPageWithLayout } from 'types'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils'
import { ProjectPageNavigation } from 'components/layouts/AuthLayout/ProjectPageNavigation'
const DatabaseTables: NextPageWithLayout = () => {
const snap = useTableEditorStateSnapshot()
@@ -50,7 +52,9 @@ const DatabaseTables: NextPageWithLayout = () => {
DatabaseTables.getLayout = (page) => (
<AppLayout>
<DefaultLayout>
<DatabaseLayout title="Database">{page}</DatabaseLayout>
<DatabaseLayout title="Database">
<ProjectPageNavigation navKey="postgres-items">{page}</ProjectPageNavigation>
</DatabaseLayout>
</DefaultLayout>
</AppLayout>
)
@@ -21,7 +21,7 @@ const PageLayout: NextPageWithLayout = () => {
if (!project) return null
return (
<div className="storage-container flex flex-grow p-4">
<div className="storage-container flex flex-grow">
{isError && <StorageBucketsError error={error as any} />}
{isSuccess ? (