mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
0bf22ee6fc
workers -> compute <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added the Compute experience for deploying, viewing, managing, and monitoring compute instances. - Added Compute navigation, instance detail pages, secrets, logs, deployment dialogs, generated snippets, and CLI commands. - Added filtering, status, availability, and data-loading support for compute instances. - **Updates** - Updated labels, icons, links, feature controls, unified logs, and secret-deletion messaging to use Compute terminology. - Compute routes now replace the previous Workers routes and pages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
487 lines
15 KiB
TypeScript
487 lines
15 KiB
TypeScript
import { LOCAL_STORAGE_KEYS, useFlag, useIsMFAEnabled, useParams } from 'common'
|
|
import { AnimatePresence, motion, MotionProps } from 'framer-motion'
|
|
import { Home } from 'icons'
|
|
import { isUndefined } from 'lodash'
|
|
import { Blocks, Boxes, ChartArea, PanelLeftDashed, Receipt, Settings, Users } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuLabel,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
Separator,
|
|
SidebarContent as SidebarContentPrimitive,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
Sidebar as SidebarPrimitive,
|
|
useSidebar,
|
|
} from 'ui'
|
|
|
|
import { Shortcut } from '../ui/Shortcut'
|
|
import { Route } from '../ui/ui.types'
|
|
import {
|
|
generateProductRoutes,
|
|
generateSettingsRoutes,
|
|
useGenerateOtherRoutes,
|
|
useGenerateToolRoutes,
|
|
} from '@/components/layouts/Navigation/NavigationBar/NavigationBar.utils'
|
|
import { ProjectIndexPageLink } from '@/data/prefetchers/project.$ref'
|
|
import { useHideSidebar } from '@/hooks/misc/useHideSidebar'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useLints } from '@/hooks/misc/useLints'
|
|
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
|
|
export const ICON_SIZE = 32
|
|
export const ICON_STROKE_WIDTH = 1.5
|
|
export type SidebarBehaviourType = 'expandable' | 'open' | 'closed'
|
|
export const DEFAULT_SIDEBAR_BEHAVIOR = 'expandable'
|
|
|
|
const SidebarMotion = motion.create(SidebarPrimitive) as FC<
|
|
ComponentProps<typeof SidebarPrimitive> & {
|
|
transition?: MotionProps['transition']
|
|
}
|
|
>
|
|
|
|
export interface SidebarProps extends ComponentPropsWithoutRef<typeof SidebarPrimitive> {}
|
|
|
|
export const Sidebar = ({ className, ...props }: SidebarProps) => {
|
|
const { setOpen } = useSidebar()
|
|
const hideSideBar = useHideSidebar()
|
|
|
|
const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
|
|
DEFAULT_SIDEBAR_BEHAVIOR
|
|
)
|
|
|
|
useEffect(() => {
|
|
// logic to toggle sidebar open based on sidebarBehaviour state
|
|
if (sidebarBehaviour === 'open') setOpen(true)
|
|
if (sidebarBehaviour === 'closed') setOpen(false)
|
|
}, [sidebarBehaviour, setOpen])
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{!hideSideBar && (
|
|
<SidebarMotion
|
|
{...props}
|
|
className={cn('z-50', className)}
|
|
transition={{ delay: 0.4, duration: 0.4 }}
|
|
overflowing={sidebarBehaviour === 'expandable'}
|
|
collapsible="icon"
|
|
variant="sidebar"
|
|
onMouseEnter={() => {
|
|
if (sidebarBehaviour === 'expandable') setOpen(true)
|
|
}}
|
|
onMouseLeave={() => {
|
|
if (sidebarBehaviour === 'expandable') setOpen(false)
|
|
}}
|
|
>
|
|
<SidebarContent
|
|
footer={
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="text"
|
|
className={`w-min px-1.5 mx-0.5 ${sidebarBehaviour === 'open' ? 'px-2!' : ''}`}
|
|
icon={<PanelLeftDashed size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
|
|
aria-label="Sidebar control"
|
|
/>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent side="top" align="start" className="w-40">
|
|
<DropdownMenuRadioGroup
|
|
value={sidebarBehaviour}
|
|
onValueChange={(value) => setSidebarBehaviour(value as SidebarBehaviourType)}
|
|
>
|
|
<DropdownMenuLabel>Sidebar control</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuRadioItem value="open">Expanded</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="closed">Collapsed</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="expandable">
|
|
Expand on hover
|
|
</DropdownMenuRadioItem>
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
}
|
|
/>
|
|
</SidebarMotion>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
export const SidebarContent = ({ footer }: { footer?: ReactNode }) => {
|
|
const { ref: projectRef } = useParams()
|
|
|
|
return (
|
|
<>
|
|
<AnimatePresence mode="wait">
|
|
<SidebarContentPrimitive>
|
|
{projectRef ? (
|
|
<motion.nav key="project-links">
|
|
<ProjectLinks />
|
|
</motion.nav>
|
|
) : (
|
|
<motion.nav
|
|
key="org-links"
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
>
|
|
<OrganizationLinks />
|
|
</motion.nav>
|
|
)}
|
|
</SidebarContentPrimitive>
|
|
</AnimatePresence>
|
|
<SidebarFooter>
|
|
<SidebarGroup className="p-0">{footer}</SidebarGroup>
|
|
</SidebarFooter>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function SideBarNavLink({
|
|
route,
|
|
active,
|
|
onClick,
|
|
isLoading,
|
|
...props
|
|
}: {
|
|
route: Route
|
|
active?: boolean
|
|
onClick?: () => void
|
|
} & ComponentPropsWithoutRef<typeof SidebarMenuButton>) {
|
|
const router = useRouter()
|
|
const { state: sidebarState } = useSidebar()
|
|
|
|
const [sidebarBehaviour] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
|
|
DEFAULT_SIDEBAR_BEHAVIOR
|
|
)
|
|
|
|
const isActiveLink = !!(route.link && !route.disabled)
|
|
const hasShortcut = !!(route.shortcutId && isActiveLink)
|
|
|
|
// Collapsed: show immediately (replaces the old label-only tooltip
|
|
// that used to surface the name of an icon-only item). Expanded:
|
|
// slight delay so the tooltip doesn't flash while skimming the nav.
|
|
const shortcutPopoverDelay = sidebarState === 'collapsed' ? 0 : 1000
|
|
|
|
const buttonProps = {
|
|
disabled: route.disabled,
|
|
isActive: active,
|
|
isLoading,
|
|
className: cn('text-sm', sidebarBehaviour === 'open' ? 'px-2!' : ''),
|
|
size: 'default' as const,
|
|
onClick: onClick,
|
|
}
|
|
|
|
const content = props.children ? (
|
|
props.children
|
|
) : (
|
|
<>
|
|
{route.icon}
|
|
<span>{route.label}</span>
|
|
{route.isNew && (
|
|
<Badge
|
|
variant="success"
|
|
className="ml-auto shrink-0 px-1.5 py-0.5 text-[9px] uppercase tracking-wider"
|
|
>
|
|
New
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)
|
|
|
|
const button = isActiveLink ? (
|
|
<SidebarMenuButton {...buttonProps} asChild>
|
|
<Link href={route.link!}>{content}</Link>
|
|
</SidebarMenuButton>
|
|
) : (
|
|
<SidebarMenuButton {...buttonProps}>{content}</SidebarMenuButton>
|
|
)
|
|
|
|
return (
|
|
<SidebarMenuItem>
|
|
{hasShortcut ? (
|
|
<Shortcut
|
|
id={route.shortcutId!}
|
|
onTrigger={() => router.push(route.link!)}
|
|
side="right"
|
|
delayDuration={shortcutPopoverDelay}
|
|
label={route.key === 'explorer' ? 'Go to Explorer' : undefined}
|
|
>
|
|
{button}
|
|
</Shortcut>
|
|
) : (
|
|
button
|
|
)}
|
|
</SidebarMenuItem>
|
|
)
|
|
}
|
|
|
|
const ActiveDot = ({ hasErrors, hasWarnings }: { hasErrors: boolean; hasWarnings: boolean }) => {
|
|
const [sidebarBehaviour] = useLocalStorageQuery(
|
|
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
|
|
DEFAULT_SIDEBAR_BEHAVIOR
|
|
)
|
|
|
|
// The nav icon only shifts right (pl-1.5 -> pl-2) when the sidebar is
|
|
// persistently open — not on hover-expand. Tie the dot to the same
|
|
// condition so it stays put while skimming the nav.
|
|
const isPersistentlyOpen = sidebarBehaviour === 'open'
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'absolute pointer-events-none flex h-2 w-2 top-2 z-10 rounded-full',
|
|
isPersistentlyOpen ? 'left-[20px]' : 'left-[18px]',
|
|
hasErrors ? 'bg-destructive-600' : hasWarnings ? 'bg-warning-600' : 'bg-transparent'
|
|
)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const ProjectLinks = () => {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
|
|
const { securityLints, errorLints } = useLints()
|
|
|
|
const activeRoute = router.pathname.split('/')[3]
|
|
|
|
const {
|
|
projectAuthAll: authEnabled,
|
|
projectEdgeFunctionAll: edgeFunctionsEnabled,
|
|
projectStorageAll: storageEnabled,
|
|
realtimeAll: realtimeEnabled,
|
|
} = useIsFeatureEnabled([
|
|
'project_auth:all',
|
|
'project_edge_function:all',
|
|
'project_storage:all',
|
|
'realtime:all',
|
|
])
|
|
|
|
const authOverviewPageEnabled = useFlag('authOverviewPage')
|
|
const computeEnabled = useFlag('compute')
|
|
|
|
const toolRoutes = useGenerateToolRoutes()
|
|
const productRoutes = generateProductRoutes(ref, project, {
|
|
auth: authEnabled,
|
|
edgeFunctions: edgeFunctionsEnabled,
|
|
storage: storageEnabled,
|
|
realtime: realtimeEnabled,
|
|
authOverviewPage: authOverviewPageEnabled,
|
|
compute: computeEnabled,
|
|
})
|
|
const otherRoutes = useGenerateOtherRoutes()
|
|
const settingsRoutes = generateSettingsRoutes(ref)
|
|
|
|
return (
|
|
<>
|
|
<SidebarGroup className="gap-0.5">
|
|
<SidebarMenu>
|
|
<SideBarNavLink
|
|
key="home"
|
|
active={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
|
|
route={{
|
|
key: 'HOME',
|
|
label: 'Project Overview',
|
|
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
link: `/project/${ref}`,
|
|
linkElement: <ProjectIndexPageLink projectRef={ref} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_HOME,
|
|
}}
|
|
/>
|
|
{toolRoutes.map((route, i) => (
|
|
<SideBarNavLink
|
|
key={`tools-routes-${i}`}
|
|
route={route}
|
|
active={activeRoute === route.key}
|
|
isLoading={isProjectPending}
|
|
/>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
<Separator className="w-[calc(100%-1rem)] mx-auto" />
|
|
<SidebarGroup className="gap-0.5">
|
|
<SidebarMenu>
|
|
{productRoutes.map((route, i) => (
|
|
<SideBarNavLink
|
|
key={`product-routes-${i}`}
|
|
route={route}
|
|
active={activeRoute === route.key}
|
|
isLoading={isProjectPending}
|
|
/>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
<Separator className="w-[calc(100%-1rem)] mx-auto" />
|
|
<SidebarGroup className="gap-0.5">
|
|
<SidebarMenu>
|
|
{otherRoutes.map((route) => {
|
|
if (route.key === 'advisors') {
|
|
return (
|
|
<SideBarNavLink
|
|
key={route.key}
|
|
route={route}
|
|
active={activeRoute === route.key}
|
|
isLoading={isProjectPending}
|
|
>
|
|
{route.icon}
|
|
<span>{route.label}</span>
|
|
{!route.disabled && (
|
|
<ActiveDot
|
|
hasErrors={errorLints.length > 0}
|
|
hasWarnings={securityLints.length > 0}
|
|
/>
|
|
)}
|
|
</SideBarNavLink>
|
|
)
|
|
} else {
|
|
return (
|
|
<SideBarNavLink
|
|
key={route.key}
|
|
route={route}
|
|
active={activeRoute === route.key}
|
|
isLoading={isProjectPending}
|
|
/>
|
|
)
|
|
}
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
<Separator className="w-[calc(100%-1rem)] mx-auto" />
|
|
{/* Settings routes to be added in with project/org nav */}
|
|
<SidebarGroup className="gap-0.5">
|
|
<SidebarMenu>
|
|
{settingsRoutes.map((route, i) => (
|
|
<SideBarNavLink
|
|
key={`settings-routes-${i}`}
|
|
route={route}
|
|
active={activeRoute === route.key}
|
|
isLoading={isProjectPending}
|
|
/>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const OrganizationLinks = () => {
|
|
const router = useRouter()
|
|
const { slug } = useParams()
|
|
|
|
const organizationSlug: string = slug ?? (router.query.orgSlug as string) ?? ''
|
|
|
|
const { data: org } = useSelectedOrganizationQuery()
|
|
const isUserMFAEnabled = useIsMFAEnabled()
|
|
const disableAccessMfa = org?.organization_requires_mfa && !isUserMFAEnabled
|
|
|
|
const showBilling = useIsFeatureEnabled('billing:all')
|
|
|
|
const activeRoute = router.pathname.split('/')[3]
|
|
const organizationSettingsRoutes = new Set([
|
|
'general',
|
|
'security',
|
|
'sso',
|
|
'apps',
|
|
'audit',
|
|
'documents',
|
|
])
|
|
|
|
const navMenuItems = [
|
|
{
|
|
label: 'Projects',
|
|
href: `/org/${organizationSlug}`,
|
|
key: 'projects',
|
|
icon: <Boxes size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_PROJECTS,
|
|
},
|
|
{
|
|
label: 'Team',
|
|
href: `/org/${organizationSlug}/team`,
|
|
key: 'team',
|
|
icon: <Users size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_TEAM,
|
|
},
|
|
{
|
|
label: 'Integrations',
|
|
href: `/org/${organizationSlug}/integrations`,
|
|
key: 'integrations',
|
|
icon: <Blocks size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_INTEGRATIONS,
|
|
},
|
|
{
|
|
label: 'Usage',
|
|
href: `/org/${organizationSlug}/usage`,
|
|
key: 'usage',
|
|
icon: <ChartArea size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_USAGE,
|
|
},
|
|
...(showBilling
|
|
? [
|
|
{
|
|
label: 'Billing',
|
|
href: `/org/${organizationSlug}/billing`,
|
|
key: 'billing',
|
|
icon: <Receipt size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_BILLING,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
label: 'Organization Settings',
|
|
href: `/org/${organizationSlug}/general`,
|
|
key: 'settings',
|
|
icon: <Settings size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
|
shortcutId: SHORTCUT_IDS.NAV_ORG_SETTINGS,
|
|
},
|
|
]
|
|
|
|
if (!organizationSlug) return null
|
|
|
|
return (
|
|
<SidebarGroup className="gap-0.5">
|
|
<SidebarMenu className="flex flex-col gap-1">
|
|
{navMenuItems.map((item, i) => (
|
|
<SideBarNavLink
|
|
key={item.key}
|
|
active={
|
|
i === 0
|
|
? activeRoute === undefined
|
|
: item.key === 'settings'
|
|
? organizationSettingsRoutes.has(activeRoute ?? '')
|
|
: activeRoute === item.key
|
|
}
|
|
route={{
|
|
label: item.label,
|
|
link: item.href,
|
|
key: item.label,
|
|
icon: item.icon,
|
|
disabled: disableAccessMfa,
|
|
shortcutId: item.shortcutId,
|
|
}}
|
|
/>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroup>
|
|
)
|
|
}
|