diff --git a/apps/studio/components/interfaces/app-default-navigation.tsx b/apps/studio/components/interfaces/app-default-navigation.tsx index 3ba4ad7cc97..b053ec3cf94 100644 --- a/apps/studio/components/interfaces/app-default-navigation.tsx +++ b/apps/studio/components/interfaces/app-default-navigation.tsx @@ -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 ( - - - - {route.icon} - {route.label} - - - - ) - // } + // console.log('route', route) + + if (!route.items) { + return ( + + + + {route.icon} + {route.label} + + + + ) + } return ( - + - {route.items.map((item) => - item.items - .filter((x) => { - if (!x.rightIcon) { - return x - } - }) - .map((subItem) => ( - - - {subItem.name} - - - )) - )} + {route.items.map((item, i) => { + // console.log('item', item) + return ( + + + {item.title} + + + ) + })} @@ -303,11 +296,12 @@ function ProjectLinks() { const otherRoutes = generateOtherRoutes(ref, project) const settingsRoutes = generateSettingsRoutes(ref, project) - console.log(productRoutes) + // console.log(productRoutes) return ( , }} /> - {toolRoutes.map((route) => ( - + {toolRoutes.map((route, i) => ( + ))} products - {productRoutes.map((route) => ( - + {productRoutes.map((route, i) => ( + ))} develop - {otherRoutes.map((route) => ( - + {otherRoutes.map((route, i) => ( + ))} - {settingsRoutes.map((route) => ( - + {settingsRoutes.map((route, i) => ( + ))} ) diff --git a/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx b/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx index e4c82397e09..ff0b96cdf26 100644 --- a/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx +++ b/apps/studio/components/layouts/AuthLayout/AuthLayout.tsx @@ -59,8 +59,7 @@ const AuthLayout = ({ title, children }: PropsWithChildren) => // productMenu={} isBlocking={false} > - - {children} + {children} ) } diff --git a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts index 3ebc0c34330..06e48c66e06 100644 --- a/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts +++ b/apps/studio/components/layouts/AuthLayout/AuthLayout.utils.ts @@ -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', }, ] } diff --git a/apps/studio/components/layouts/AuthLayout/ProjectPageNavigation.tsx b/apps/studio/components/layouts/AuthLayout/ProjectPageNavigation.tsx index cdfb40cf932..a1ddeb5c5c7 100644 --- a/apps/studio/components/layouts/AuthLayout/ProjectPageNavigation.tsx +++ b/apps/studio/components/layouts/AuthLayout/ProjectPageNavigation.tsx @@ -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 ( - - {items.map((item) => { - return ( - <> - {/* {item.title} */} - {item.items && - item.items.map((item) => { - return ( + <> + + {items.map((item) => { + const childIsActive = params[item.childId] && pathname?.includes(item.url) + + return ( + <> + + + {item.name} + + + + {item.hasChild && params[item.childId] && ( <> - - - {item.name} + + + + {item.childIcon} + {params[item.childId]} - + > - ) - })} - > - ) - })} - + )} + + > + ) + })} + + {children} + > ) } diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 155ff39b98c..2e06a1c478b 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -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 ( - <> - - > - ) -} +// return ( +// <> +// +// > +// ) +// } const DatabaseLayout = ({ children }: PropsWithChildren) => { + const { ref } = useParams() return ( { 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: , - 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: , - items: [], - }, - { - name: 'Performance Advisor', - key: 'performance-advisor', - url: `/project/${ref}/advisors/performance`, - rightIcon: , - items: [], - }, - { - name: 'Query Performance', - key: 'query-performance', - url: `/project/${ref}/advisors/query-performance`, + name: 'Policies', + key: 'policies', + url: `/project/${ref}/auth/policies`, rightIcon: , 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: , + // items: [], + // }, + // { + // name: 'Performance Advisor', + // key: 'performance-advisor', + // url: `/project/${ref}/advisors/performance`, + // rightIcon: , + // items: [], + // }, + // { + // name: 'Query Performance', + // key: 'query-performance', + // url: `/project/${ref}/advisors/query-performance`, + // rightIcon: , + // 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: , + }, + // ...(!!pgNetExtensionExists + // ? [ + { + name: 'Webhooks', + key: 'hooks', + url: `/project/${ref}/integrations/webhooks/overview`, + rightIcon: , + items: [], + }, + // ] + // : []), ] } diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 95ff6239fe6..ccc8f8e2d6d 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -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: , 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: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/auth/users`), - items: generateAuthMenu(ref as string), + // items: authMenu, }, ] : []), diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx index b5d8eff7df0..d76ee09d61e 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.tsx @@ -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 ( }> - {children} + {children} ) } diff --git a/apps/studio/components/layouts/StorageLayout/StorageLayout.utils.tsx b/apps/studio/components/layouts/StorageLayout/StorageLayout.utils.tsx new file mode 100644 index 00000000000..96fc397545d --- /dev/null +++ b/apps/studio/components/layouts/StorageLayout/StorageLayout.utils.tsx @@ -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: ( + + ), + }, + { + 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`, + }, + ] +} diff --git a/apps/studio/components/layouts/page-tabs.tsx b/apps/studio/components/layouts/page-tabs.tsx new file mode 100644 index 00000000000..353f653f8c5 --- /dev/null +++ b/apps/studio/components/layouts/page-tabs.tsx @@ -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 & 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 + 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(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 ( + + + + {isSticky && ( + + {integration?.icon({ + style: { padding: iconPadding.get() }, + })} + + )} + + {tabs.map((tab) => { + const tabUrl = `/project/${project?.ref}/integrations/${integration?.id}/${tab.route}` + return ( + + + {tab.label} + + + + {tab.hasChild && childId && ( + <> + + + + + + {tab.childIcon} + {childId} + + + > + )} + + + ) + })} + + + + ) +} + +IntegrationTabs.displayName = 'IntegrationTabs' diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index edd72f26b09..e3ccc7e6edf 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -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 ? ( { const snap = useTableEditorStateSnapshot() @@ -50,7 +52,9 @@ const DatabaseTables: NextPageWithLayout = () => { DatabaseTables.getLayout = (page) => ( - {page} + + {page} + ) diff --git a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx index 8b9b2e85ea1..ebb8ff268ec 100644 --- a/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx +++ b/apps/studio/pages/project/[ref]/storage/buckets/[bucketId].tsx @@ -21,7 +21,7 @@ const PageLayout: NextPageWithLayout = () => { if (!project) return null return ( - + {isError && } {isSuccess ? (