mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
moar
This commit is contained in:
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 ? (
|
||||
|
||||
Reference in new issue
Block a user