global user dropdown in docs and www (#35063)

* docs: user nav dropdown

* www: user dropdown nav

* update menus

* chore: add complete local storage allowlist

* move all local-storage to common

* reload after logOut

* add local storage key changes from #35175

* fix errors

* add more keys

* fix merge bugs

---------

Co-authored-by: Alaister Young <a@alaisteryoung.com>
This commit is contained in:
Francesco Sansalvadore
2025-05-05 11:48:06 +02:00
committed by GitHub
parent 24afbe0497
commit a4cfcd9b2e
63 changed files with 539 additions and 629 deletions

View File

@@ -1,15 +1,17 @@
import { Command, Search, Menu } from 'lucide-react'
import { memo, useState } from 'react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import Link from 'next/link'
import type { FC } from 'react'
import { memo, useState } from 'react'
import { Command, Search, Menu } from 'lucide-react'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { useIsLoggedIn, useIsUserLoading, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import { CommandMenuTrigger } from 'ui-patterns/CommandMenu'
import { AuthenticatedDropdownMenu, CommandMenuTrigger } from 'ui-patterns'
import GlobalNavigationMenu from './GlobalNavigationMenu'
import useDropdownMenu from './useDropdownMenu'
import type { FC } from 'react'
const GlobalMobileMenu = dynamic(() => import('./GlobalMobileMenu'))
const TopNavDropdown = dynamic(() => import('./TopNavDropdown'))
@@ -17,6 +19,8 @@ const TopNavBar: FC = () => {
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const user = useUser()
const menu = useDropdownMenu(user)
return (
<>
@@ -95,7 +99,11 @@ const TopNavBar: FC = () => {
<Link href="/dev-secret-auth">Dev-only secret sign-in</Link>
</Button>
)}
<TopNavDropdown />
{isLoggedIn ? (
<AuthenticatedDropdownMenu menu={menu} user={user} site="docs" />
) : (
<TopNavDropdown />
)}
</div>
</div>
</nav>

View File

@@ -19,7 +19,6 @@ import {
cn,
themes,
} from 'ui'
import MenuIconPicker from './MenuIconPicker'
const menu = [
@@ -70,7 +69,7 @@ const TopNavDropdown = () => {
<Menu size={18} strokeWidth={1} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-64">
<DropdownMenuContent side="bottom" align="end" className="w-52">
{menu.map((menuSection, sectionIdx) => (
<Fragment key={`topnav--${sectionIdx}`}>
{sectionIdx !== 0 && <DropdownMenuSeparator key={`topnav--${sectionIdx}`} />}

View File

@@ -0,0 +1,80 @@
'use client'
import type { User } from '@supabase/supabase-js'
import { LogOut, Globe, LifeBuoy, Settings, UserIcon, Database } from 'lucide-react'
import { logOut } from 'common'
import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu'
import { IconGitHub } from './MenuIcons'
const useDropdownMenu = (user: User | null) => {
const menu: menuItem[][] = [
[
{
label: user?.email ?? "You're logged in",
type: 'text',
icon: UserIcon,
},
{
label: 'Account Preferences',
icon: Settings,
href: 'https://supabase.com/dashboard/account/me',
},
{
label: 'All Projects',
icon: Database,
href: 'https://supabase.com/dashboard/projects',
},
],
[
{
label: 'Supabase.com',
icon: Globe,
href: 'https://supabase.com',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'GitHub',
icon: IconGitHub as any,
href: 'https://github.com/supabase/supabase',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'Support',
icon: LifeBuoy,
href: 'https://supabase.com/support',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
],
[
{
label: 'Theme',
type: 'theme',
},
],
[
{
label: 'Logout',
type: 'button',
icon: LogOut,
onClick: async () => {
await logOut()
window.location.reload()
},
},
],
]
return menu
}
export default useDropdownMenu

View File

@@ -15,7 +15,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'
import { withErrorBoundary } from 'react-error-boundary'
import { proxy, useSnapshot } from 'valtio'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common'
import { Button_Shadcn_ as Button, Input_Shadcn_ as Input, cn } from 'ui'
import {
@@ -36,7 +36,7 @@ import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
import { useProjectApiQuery } from '~/lib/fetch/projectApi'
import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects'
import { LOCAL_STORAGE_KEYS, retrieve, storeOrRemoveNull } from '~/lib/storage'
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
type ProjectOrgDataState =

View File

@@ -1,9 +1,9 @@
'use client'
import { useQueryClient } from '@tanstack/react-query'
import { AuthProvider } from 'common'
import { AuthProvider, LOCAL_STORAGE_KEYS } from 'common'
import { type PropsWithChildren, useCallback } from 'react'
import { LOCAL_STORAGE_KEYS, remove } from '~/lib/storage'
import { remove } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
/**

View File

@@ -1,11 +1,6 @@
export const LOCAL_STORAGE_KEYS = {
SAVED_ORG: 'docs.ui.user.selected.org',
SAVED_PROJECT: 'docs.ui.user.selected.project',
SAVED_BRANCH: 'docs.ui.user.selected.branch',
} as const
import { LOCAL_STORAGE_KEYS } from 'common'
type LocalStorageKey = (typeof LOCAL_STORAGE_KEYS)[keyof typeof LOCAL_STORAGE_KEYS]
type StorageType = 'local' | 'session'
function getStorage(storageType: StorageType) {
@@ -17,7 +12,7 @@ export function store(storageType: StorageType, key: LocalStorageKey, value: str
const storage = getStorage(storageType)
try {
storage.setItem(key, value)
storage.setItem(key as string, value)
} catch {
console.error(`Failed to set storage item with key "${key}"`)
}
@@ -26,13 +21,13 @@ export function store(storageType: StorageType, key: LocalStorageKey, value: str
export function retrieve(storageType: StorageType, key: LocalStorageKey) {
if (typeof window === 'undefined') return
const storage = getStorage(storageType)
return storage.getItem(key)
return storage.getItem(key as string)
}
export function remove(storageType: StorageType, key: LocalStorageKey) {
if (typeof window === 'undefined') return
const storage = getStorage(storageType)
return storage.removeItem(key)
return storage.removeItem(key as string)
}
export function storeOrRemoveNull(

View File

@@ -1,6 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
@@ -27,6 +26,7 @@ import {
Input_Shadcn_,
Separator,
} from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
const setDeletionRequestFlag = () => {
const expiryDate = new Date()

View File

@@ -5,7 +5,7 @@ import SVG from 'react-inlinesvg'
import { DEFAULT_SIDEBAR_BEHAVIOR } from 'components/interfaces/Sidebar'
import Panel from 'components/ui/Panel'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import {
Label_Shadcn_,
RadioGroup_Shadcn_,
@@ -20,6 +20,7 @@ import {
Theme,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { LOCAL_STORAGE_KEYS } from 'common'
export const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)

View File

@@ -1,8 +1,7 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
const FLY_POSTGRES_DEPRECATION_WARNING_KEY = LOCAL_STORAGE_KEYS.FLY_POSTGRES_DEPRECATION_WARNING
// [Joshen] This file is meant to be dynamic - update this as and when we need to use the NoticeBanner

View File

@@ -1,8 +1,7 @@
import { noop } from 'lodash'
import { FeatureFlagContext } from 'common'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { APISidePanelPreview } from './APISidePanelPreview'
@@ -11,6 +10,7 @@ import { InlineEditorPreview } from './InlineEditorPreview'
import { LayoutUpdatePreview } from './LayoutUpdatePreview'
import { SqlEditorTabsPreview } from './SqlEditorTabs'
import { TableEditorTabsPreview } from './TableEditorTabs'
import { LOCAL_STORAGE_KEYS } from 'common'
export const FEATURE_PREVIEWS = [
{

View File

@@ -2,10 +2,11 @@ import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { removeTabsByEditor } from 'state/tabs'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'

View File

@@ -1,10 +1,11 @@
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH } from 'lib/constants'
import { ExternalLink, X } from 'lucide-react'
import Image from 'next/image'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Badge, Button } from 'ui'
import { useIsNewLayoutEnabled } from './FeaturePreviewContext'
import { LOCAL_STORAGE_KEYS } from 'common'
export const LayoutUpdatePreview = () => {
return (

View File

@@ -2,13 +2,13 @@ import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { toast } from 'sonner'
import { useIsLoggedIn, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import useLatest from 'hooks/misc/useLatest'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsNewLayoutEnabled } from './FeaturePreview/FeaturePreviewContext'

View File

@@ -2,7 +2,6 @@ import { isEmpty, noop } from 'lodash'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Modal } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'

View File

@@ -5,7 +5,7 @@ import { UIEvent, useEffect, useMemo, useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
@@ -18,7 +18,6 @@ import { useUserDeleteMutation } from 'data/auth/user-delete-mutation'
import { useUsersCountQuery } from 'data/auth/users-count-query'
import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
Button,
cn,

View File

@@ -6,7 +6,7 @@ import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import AlertError from 'components/ui/AlertError'
@@ -16,7 +16,6 @@ import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { toast } from 'sonner'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import { SchemaGraphLegend } from './SchemaGraphLegend'

View File

@@ -4,9 +4,9 @@ import { uniqBy } from 'lodash'
import { Edge, Node, Position } from 'reactflow'
import 'reactflow/dist/style.css'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH, TableNodeData } from './SchemaTableNode'
import { LOCAL_STORAGE_KEYS } from 'common'
const NODE_SEP = 25
const RANK_SEP = 50

View File

@@ -37,10 +37,10 @@ import { MouseEventHandler, useCallback, useEffect, useState } from 'react'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, cn } from 'ui'
import { RoleImpersonationSelector } from '../RoleImpersonationSelector'
import styles from './graphiql.module.css'
import { LOCAL_STORAGE_KEYS } from 'common'
export interface GraphiQLProps {
fetcher: Fetcher

View File

@@ -3,12 +3,12 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useOrganizationDeleteMutation } from 'data/organizations/organization-delete-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Button, Form, Input, Modal } from 'ui'
const DeleteOrganizationButton = () => {

View File

@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
ScaffoldActionsContainer,
@@ -22,7 +22,6 @@ import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'

View File

@@ -4,14 +4,14 @@ import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { formatDatabaseID } from 'data/read-replicas/replicas.utils'
import { executeSql } from 'data/sql/execute-sql-query'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,

View File

@@ -2,14 +2,13 @@ import { ArrowDown, ArrowUp, RefreshCw } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { DownloadResultsButton } from 'components/ui/DownloadResultsButton'
import { FilterPopover } from 'components/ui/FilterPopover'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
Button,
DropdownMenu,

View File

@@ -3,10 +3,9 @@ import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { MutableRefObject, useEffect, useRef } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import ResizableAIWidget from 'components/ui/AIEditor/ResizableAIWidget'
import { GridFooter } from 'components/ui/GridFooter'
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
@@ -24,7 +24,7 @@ import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi'
import { useSchemasForAi } from 'hooks/misc/useSchemasForAi'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { BASE_PATH, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { formatSql } from 'lib/formatSql'
import { detectOS, uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'

View File

@@ -2,13 +2,12 @@ import dayjs from 'dayjs'
import { ArrowUpDown, X } from 'lucide-react'
import { useMemo } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import BarChart from 'components/ui/Charts/BarChart'
import NoDataPlaceholder from 'components/ui/Charts/NoDataPlaceholder'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import Link from 'next/link'
import {
Badge,

View File

@@ -1,11 +1,11 @@
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { detectOS } from 'lib/helpers'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {

View File

@@ -1,4 +1,5 @@
import { Monaco } from '@monaco-editor/react'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import getPgsqlCompletionProvider from 'components/ui/CodeEditor/Providers/PgSQLCompletionProvider'
import getPgsqlSignatureHelpProvider from 'components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider'
@@ -7,7 +8,6 @@ import { useKeywordsQuery } from 'data/database/keywords-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTableColumnsQuery } from 'data/database/table-columns-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { formatSql } from 'lib/formatSql'
import { IDisposable } from 'monaco-editor'
import { useEffect, useRef } from 'react'

View File

@@ -14,7 +14,7 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, ComponentPropsWithoutRef, FC, ReactNode, useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
generateOtherRoutes,
generateProductRoutes,
@@ -30,7 +30,6 @@ import { useLints } from 'hooks/misc/useLints'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { Home } from 'icons'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,

View File

@@ -7,7 +7,8 @@ import { PropsWithChildren, useEffect } from 'react'
import { useIsNewLayoutEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useAppStateSnapshot } from 'state/app-state'
import { cn, NavMenu, NavMenuItem } from 'ui'
import {

View File

@@ -6,9 +6,9 @@ import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2'
import { LOCAL_STORAGE_KEYS } from 'common'
interface LogsLayoutProps {
title?: string

View File

@@ -7,7 +7,8 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useShowLayoutHeader } from 'hooks/misc/useShowLayoutHeader'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
export const HomeIcon = () => {
const newLayoutPreview = useIsNewLayoutEnabled()

View File

@@ -11,7 +11,7 @@ import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import type { Route } from 'components/ui/ui.types'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
route: Route

View File

@@ -1,15 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { getAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'

View File

@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import DownloadSnippetModal from 'components/interfaces/SQLEditor/DownloadSnippetModal'
import { MoveQueryModal } from 'components/interfaces/SQLEditor/MoveQueryModal'
@@ -17,7 +17,6 @@ import { Snippet, SnippetFolder, useSQLSnippetFoldersQuery } from 'data/content/
import { useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import uuidv4 from 'lib/uuid'
import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react'

View File

@@ -16,8 +16,7 @@ import {
TabsTrigger_Shadcn_,
} from 'ui'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
const ApiKeysLayout = ({ children }: PropsWithChildren) => {
const { ref: projectRef } = useParams()

View File

@@ -3,12 +3,13 @@ import * as Sentry from '@sentry/nextjs'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams, useTelemetryCookie, useUser } from 'common'
import { LOCAL_STORAGE_KEYS, useParams, useTelemetryCookie, useUser } from 'common'
import { useSendGroupsIdentifyMutation } from 'data/telemetry/send-groups-identify-mutation'
import { useSendGroupsResetMutation } from 'data/telemetry/send-groups-reset-mutation'
import { usePrevious } from 'hooks/deprecated'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { IS_PLATFORM } from 'lib/constants'
const getAnonId = async (id: string) => {
const encoder = new TextEncoder()

View File

@@ -3,7 +3,7 @@ import { toast } from 'sonner'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
export type GitHubAuthorizationCreateVariables = {
code: string

View File

@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorage } from './useLocalStorage'
export type LastSignInType = 'github' | 'email' | 'sso' | null

View File

@@ -1,8 +1,7 @@
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
/**
* This hook wraps useQueryState because useQueryState imports app router for some reason which breaks the SSR in

View File

@@ -1,5 +1,5 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { Dispatch, SetStateAction } from 'react'
/**

View File

@@ -1,10 +1,9 @@
import { useQueryClient } from '@tanstack/react-query'
import { AuthProvider as AuthProviderInternal, gotrueClient } from 'common'
import { AuthProvider as AuthProviderInternal, clearLocalStorage, gotrueClient } from 'common'
import { PropsWithChildren, useCallback, useEffect } from 'react'
import { toast } from 'sonner'
import { GOTRUE_ERRORS, IS_PLATFORM } from './constants'
import { clearLocalStorage } from './local-storage'
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
// Check for unverified GitHub users after a GitHub sign in

View File

@@ -38,65 +38,6 @@ export const STRIPE_PUBLIC_KEY =
export const USAGE_APPROACHING_THRESHOLD = 0.75
export const LOCAL_STORAGE_KEYS = {
AI_ASSISTANT_STATE: (projectRef: string | undefined) =>
`supabase-ai-assistant-state-${projectRef}`,
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs',
UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs',
UI_NEW_LAYOUT_PREVIEW: 'supabase-ui-new-layout-preview',
NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
STORAGE_PREFERENCE: (ref: string) => `storage-explorer-${ref}`,
SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled',
SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size',
// Key to track which schemas are ok to be sent to AI. The project ref is intentionally put at the end for easier search in the browser console.
SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`,
SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open',
SQL_EDITOR_LAST_SELECTED_DB: (ref: string) => `sql-editor-last-selected-db-${ref}`,
SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`,
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
GRAPHIQL_RLS_BYPASS_WARNING: 'graphiql-rls-bypass-warning-dismissed',
CLS_DIFF_WARNING: 'cls-diff-warning-dismissed',
CLS_SELECT_STAR_WARNING: 'cls-select-star-warning-dismissed',
QUERY_PERF_SHOW_BOTTOM_SECTION: 'supabase-query-perf-show-bottom-section',
// Key to track account deletion requests
ACCOUNT_DELETION_REQUEST: 'supabase-account-deletion-request',
// Used for storing a user id when sending reports to Sentry. The id is hashed for anonymity.
SENTRY_USER_ID: 'supabase-sentry-user-id',
// Used for storing the last sign in method used by the user
LAST_SIGN_IN_METHOD: 'supabase-last-sign-in-method',
// Key to track the last selected schema. The project ref is intentionally put at the end for easier search in the browser console.
LAST_SELECTED_SCHEMA: (ref: string) => `last-selected-schema-${ref}`,
// Track position of nodes for schema visualizer
SCHEMA_VISUALIZER_POSITIONS: (ref: string, schemaId: number) =>
`schema-visualizer-positions-${ref}-${schemaId}`,
// Used for allowing the main nav panel to expand on hover
EXPAND_NAVIGATION_PANEL: 'supabase-expand-navigation-panel',
GITHUB_AUTHORIZATION_STATE: 'supabase-github-authorization-state',
// Notice banner keys
FLY_POSTGRES_DEPRECATION_WARNING: 'fly-postgres-deprecation-warning-dismissed',
AUTH_USERS_COLUMNS_CONFIGURATION: (ref: string) => `supabase-auth-users-columns-${ref}`,
// api keys view switcher for new and legacy api keys
API_KEYS_VIEW: (ref: string) => `supabase-api-keys-view-${ref}`,
// last visited logs page
LAST_VISITED_LOGS_PAGE: 'supabase-last-visited-logs-page',
LAST_VISITED_ORGANIZATION: 'last-visited-organization',
}
export const OPT_IN_TAGS = {
AI_SQL: 'AI_SQL_GENERATOR_OPT_IN',
}

View File

@@ -1,4 +1,4 @@
import { LOCAL_STORAGE_KEYS } from './constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { makeRandomString } from './helpers'
const GITHUB_INTEGRATION_APP_NAME =

View File

@@ -1,27 +0,0 @@
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
const LOCAL_STORAGE_KEYS_ALLOWLIST = [
'graphiql:theme',
'theme',
'supabaseDarkMode',
'supabase.dashboard.auth.debug',
'supabase.dashboard.auth.navigatorLock.disabled',
COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT,
LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS,
LOCAL_STORAGE_KEYS.LAST_SIGN_IN_METHOD,
]
export function clearLocalStorage() {
for (const key in localStorage) {
if (!LOCAL_STORAGE_KEYS_ALLOWLIST.includes(key)) {
localStorage.removeItem(key)
}
}
}

View File

@@ -1,7 +1,7 @@
import { components } from 'api-types'
import { hasConsented } from 'common'
import { handleError, post } from 'data/fetchers'
import { IS_PLATFORM } from './constants'
import { LOCAL_STORAGE_KEYS, hasConsented } from 'common'
type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBodyDto']

View File

@@ -42,11 +42,12 @@ import { downloadBucketObject } from 'data/storage/bucket-object-download-mutati
import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation'
import { Bucket } from 'data/storage/buckets-query'
import { moveStorageObject } from 'data/storage/object-move-mutation'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { IS_PLATFORM } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { lookupMime } from 'lib/mime'
import Link from 'next/link'
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
type CachedFile = { id: string; fetchedAt: number; expiresIn: number; url: string }

View File

@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AlertCircle, XIcon } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
@@ -27,7 +27,6 @@ import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'

View File

@@ -6,7 +6,8 @@ import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { IS_PLATFORM, useParams } from 'common'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
import {
LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD,
LOGS_TABLES,
@@ -41,7 +42,6 @@ import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import type { LogSqlSnippets, NextPageWithLayout } from 'types'

View File

@@ -1,11 +1,10 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {

View File

@@ -1,5 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import LegacyAPIKeys from 'components/interfaces/APIKeys/LegacyAPIKeys'
import { PublishableAPIKeys } from 'components/interfaces/APIKeys/PublishableAPIKeys'
@@ -9,7 +9,6 @@ import ApiKeysLayout from 'components/layouts/project/[ref]/settings/APIKeysLayo
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'

View File

@@ -13,9 +13,10 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, PROJECT_STATUS } from 'lib/constants'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { cn } from 'ui'
import { LOCAL_STORAGE_KEYS } from 'common'
const ProjectsPage: NextPageWithLayout = () => {
const newLayoutPreview = useIsNewLayoutEnabled()

View File

@@ -3,8 +3,7 @@ import type { Message as MessageType } from 'ai/react'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { debounce } from 'lodash'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
type SuggestionsType = {
title: string

View File

@@ -1,7 +1,7 @@
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS, LOCAL_STORAGE_KEYS } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
export type Template = {
name: string

View File

@@ -8,7 +8,7 @@ import {
STORAGE_SORT_BY_ORDER,
STORAGE_VIEWS,
} from 'components/to-be-cleaned/Storage/Storage.constants'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { LOCAL_STORAGE_KEYS } from 'common'
import { tryParseJson } from 'lib/helpers'
const DEFAULT_PREFERENCES = {

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { useWindowSize } from 'react-use'
import { useIsLoggedIn } from 'common'
import { useIsLoggedIn, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import {
NavigationMenu,
@@ -23,6 +23,8 @@ import MenuItem from './MenuItem'
import MobileMenu from './MobileMenu'
import RightClickBrandLogo from './RightClickBrandLogo'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import useDropdownMenu from './useDropdownMenu'
import { AuthenticatedDropdownMenu } from 'ui-patterns'
interface Props {
hideNavbar: boolean
@@ -37,6 +39,8 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
const isLoggedIn = useIsLoggedIn()
const menu = getMenu()
const sendTelemetryEvent = useSendTelemetryEvent()
const user = useUser()
const userMenu = useDropdownMenu(user)
const isHomePage = router.pathname === '/'
const isLaunchWeekPage = router.pathname.includes('/launch-week')
@@ -130,9 +134,12 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
<GitHubButton />
{isLoggedIn ? (
<Button className="hidden lg:block" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
<>
<Button className="hidden lg:block" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
<AuthenticatedDropdownMenu menu={userMenu} user={user} site="www" />
</>
) : (
<>
<Button type="default" className="hidden lg:block" asChild>

View File

@@ -0,0 +1,62 @@
'use client'
import type { User } from '@supabase/supabase-js'
import { Command, Database, LogOut, Settings, UserIcon } from 'lucide-react'
import { logOut } from 'common'
import type { menuItem } from 'ui-patterns/AuthenticatedDropdownMenu'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { useRouter } from 'next/router'
const useDropdownMenu = (user: User | null) => {
const router = useRouter()
const setCommandMenuOpen = useSetCommandMenuOpen()
const menu: menuItem[][] = [
[
{
label: user?.email ?? "You're logged in",
type: 'text',
icon: UserIcon,
},
{
label: 'Account Preferences',
icon: Settings,
href: 'https://supabase.com/dashboard/account/me',
},
{
label: 'All Projects',
icon: Database,
href: 'https://supabase.com/dashboard/projects',
},
{
label: 'Command Menu',
icon: Command,
type: 'button',
onClick: () => setCommandMenuOpen(true),
shortcut: '⌘K',
},
],
[
{
label: 'Theme',
type: 'theme',
},
],
[
{
label: 'Logout',
type: 'button',
icon: LogOut,
onClick: async () => {
await logOut()
router.reload()
},
},
],
]
return menu
}
export default useDropdownMenu

View File

@@ -11,6 +11,7 @@ import {
useState,
} from 'react'
import { gotrueClient, type User } from './gotrue'
import { clearLocalStorage } from './constants/local-storage'
export type { User }
@@ -115,6 +116,13 @@ export const useIsLoggedIn = () => {
return user !== null
}
export const signOut = async () => await gotrueClient.signOut()
export const logOut = async () => {
await signOut()
clearLocalStorage()
}
let currentSession: Session | null = null
gotrueClient.onAuthStateChange((event, session) => {

View File

@@ -1,7 +1,111 @@
export const LOCAL_STORAGE_KEYS = {
/**
* STUDIO
*/
AI_ASSISTANT_STATE: (projectRef: string | undefined) =>
`supabase-ai-assistant-state-${projectRef}`,
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs',
UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs',
UI_NEW_LAYOUT_PREVIEW: 'supabase-ui-new-layout-preview',
NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
STORAGE_PREFERENCE: (ref: string) => `storage-explorer-${ref}`,
SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled',
SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size',
// Key to track which schemas are ok to be sent to AI. The project ref is intentionally put at the end for easier search in the browser console.
SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`,
SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open',
SQL_EDITOR_LAST_SELECTED_DB: (ref: string) => `sql-editor-last-selected-db-${ref}`,
SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`,
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
GRAPHIQL_RLS_BYPASS_WARNING: 'graphiql-rls-bypass-warning-dismissed',
CLS_DIFF_WARNING: 'cls-diff-warning-dismissed',
CLS_SELECT_STAR_WARNING: 'cls-select-star-warning-dismissed',
QUERY_PERF_SHOW_BOTTOM_SECTION: 'supabase-query-perf-show-bottom-section',
// Key to track account deletion requests
ACCOUNT_DELETION_REQUEST: 'supabase-account-deletion-request',
// Used for storing a user id when sending reports to Sentry. The id is hashed for anonymity.
SENTRY_USER_ID: 'supabase-sentry-user-id',
// Used for storing the last sign in method used by the user
LAST_SIGN_IN_METHOD: 'supabase-last-sign-in-method',
// Key to track the last selected schema. The project ref is intentionally put at the end for easier search in the browser console.
LAST_SELECTED_SCHEMA: (ref: string) => `last-selected-schema-${ref}`,
// Track position of nodes for schema visualizer
SCHEMA_VISUALIZER_POSITIONS: (ref: string, schemaId: number) =>
`schema-visualizer-positions-${ref}-${schemaId}`,
// Used for allowing the main nav panel to expand on hover
EXPAND_NAVIGATION_PANEL: 'supabase-expand-navigation-panel',
GITHUB_AUTHORIZATION_STATE: 'supabase-github-authorization-state',
// Notice banner keys
FLY_POSTGRES_DEPRECATION_WARNING: 'fly-postgres-deprecation-warning-dismissed',
AUTH_USERS_COLUMNS_CONFIGURATION: (ref: string) => `supabase-auth-users-columns-${ref}`,
// api keys view switcher for new and legacy api keys
API_KEYS_VIEW: (ref: string) => `supabase-api-keys-view-${ref}`,
// last visited logs page
LAST_VISITED_LOGS_PAGE: 'supabase-last-visited-logs-page',
LAST_VISITED_ORGANIZATION: 'last-visited-organization',
/**
* COMMON
*/
/** @deprecated we're using usercentrics instead to handle telemetry consent */
TELEMETRY_CONSENT: 'supabase-consent-ph',
TELEMETRY_DATA: 'supabase-telemetry-data',
/**
* DOCS
*/
SAVED_ORG: 'docs.ui.user.selected.org',
SAVED_PROJECT: 'docs.ui.user.selected.project',
SAVED_BRANCH: 'docs.ui.user.selected.branch',
HIDE_PROMO_TOAST: 'supabase-hide-promo-toast-lw13-d1',
/**
* WWW
*/
BLOG_VIEW: 'supabase-blog-view',
} as const
export type LocalStorageKey = (typeof LOCAL_STORAGE_KEYS)[keyof typeof LOCAL_STORAGE_KEYS]
const LOCAL_STORAGE_KEYS_ALLOWLIST = [
'graphiql:theme',
'theme',
'supabaseDarkMode',
'supabase.dashboard.auth.debug',
'supabase.dashboard.auth.navigatorLock.disabled',
LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT,
LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
LOCAL_STORAGE_KEYS.UI_NEW_LAYOUT_PREVIEW,
LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS,
LOCAL_STORAGE_KEYS.LAST_SIGN_IN_METHOD,
LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST,
LOCAL_STORAGE_KEYS.BLOG_VIEW,
]
export function clearLocalStorage() {
for (const key in localStorage) {
if (!LOCAL_STORAGE_KEYS_ALLOWLIST.includes(key)) {
localStorage.removeItem(key)
}
}
}

View File

@@ -0,0 +1,169 @@
'use client'
import { Fragment } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { useTheme } from 'next-themes'
import { UserIcon } from 'lucide-react'
import {
cn,
buttonVariants,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from 'ui'
import { themes } from 'ui/src/components/ThemeProvider/themes'
import type { User } from '@supabase/supabase-js'
import type { LucideIcon } from 'icons/src/createSupabaseIcon'
import type { Theme } from 'ui/src/components/ThemeProvider/themes'
interface Props {
menu: menuItem[][]
user: User | null
hasThemeSelector?: boolean
site?: 'docs' | 'www' | 'studio'
}
export interface menuItem {
label: string
type?: 'link' | 'button' | 'text' | 'theme'
icon?: LucideIcon
href?: string
shortcut?: string
onClick?: VoidFunction
otherProps?: {
target?: '_blank'
rel?: 'noreferrer noopener'
}
}
export const AuthenticatedDropdownMenu = ({ user, menu, site }: Props) => {
const { theme, setTheme } = useTheme()
const userAvatar = user && user.user_metadata?.avatar_url
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className="flex">
<button
title="Menu dropdown button"
className={cn(
buttonVariants({ type: 'default' }),
'text-foreground-light border-default w-[30px] min-w-[30px] h-[30px] data-[state=open]:bg-overlay-hover/30 hover:border-strong data-[state=open]:border-stronger hover:!bg-overlay-hover/50 bg-transparent',
'rounded-full overflow-hidden opacity-0 transition-opacity animate-fade-in'
)}
>
{userAvatar ? (
<Image
src={userAvatar}
alt={user?.email ?? ''}
placeholder="blur"
blurDataURL="/images/blur.png"
fill
sizes="30px"
className="object-cover object-center"
/>
) : (
<UserIcon size={16} strokeWidth={1.5} />
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-52">
{menu.map((menuSection, sectionIdx) => (
<Fragment key={`${site}-auth-dropdown--${sectionIdx}`}>
{sectionIdx !== 0 && (
<DropdownMenuSeparator key={`${site}-auth-dropdown--${sectionIdx}`} />
)}
{menuSection.map((sectionItem, itemIdx) => {
switch (sectionItem.type) {
case 'text':
return (
<div
key={`${site}-auth-dropdown-${sectionItem.label}-${sectionIdx}-${itemIdx}`}
className="flex cursor-text items-center text-foreground rounded-sm px-2 py-1.5 text-xs outline-none space-x-2"
{...sectionItem.otherProps}
>
<DropdownItemContent {...sectionItem} />
</div>
)
case 'button':
return (
<DropdownMenuItem
key={`${site}-auth-dropdown-${sectionItem.label}-${sectionIdx}-${itemIdx}`}
className="space-x-2 hover:cursor-pointer"
onClick={sectionItem.onClick!}
{...sectionItem.otherProps}
>
<DropdownItemContent {...sectionItem} />
</DropdownMenuItem>
)
case 'theme':
return (
<>
<DropdownMenuLabel
key={`${site}-auth-dropdown-${sectionItem.label}-${sectionIdx}-${itemIdx}`}
>
{sectionItem.label}
</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={theme}
onValueChange={(value) => {
setTheme(value)
}}
>
{themes
.filter(
(x) => x.value === 'light' || x.value === 'dark' || x.value === 'system'
)
.map((theme: Theme) => (
<DropdownMenuRadioItem
key={`${site}-auth-dropdown-theme-${theme.value}`}
value={theme.value}
className="hover:cursor-pointer"
>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</>
)
case 'link':
default:
return (
<Link
key={`${site}-auth-dropdown-${sectionItem.label}-${sectionIdx}-${itemIdx}`}
href={sectionItem.href!}
{...sectionItem.otherProps}
>
<DropdownMenuItem
className="space-x-2 hover:cursor-pointer"
onClick={() => {}}
>
<DropdownItemContent {...sectionItem} />
</DropdownMenuItem>
</Link>
)
}
})}
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
const DropdownItemContent = ({ icon: Icon, ...sectionItem }: menuItem) => (
<>
{Icon && <Icon className="w-3 h-3" />}
<span className="grow truncate">{sectionItem.label}</span>
{sectionItem.shortcut && <DropdownMenuShortcut>{sectionItem.shortcut}</DropdownMenuShortcut>}
</>
)
export default AuthenticatedDropdownMenu

View File

@@ -2,26 +2,28 @@
* The components are listed here so that VsCode can find out about them and list them as import suggestions. Don't
* import directly from here.
*/
export * from './admonition'
export * from './AssistantChat/AssistantChatForm'
export * from './AssistantChat/AssistantCommandsPopover'
export * from './AuthenticatedDropdownMenu'
export * from './CommandMenu'
export * from './ComputeBadge'
export * from './ConsentToast'
export * from './consent'
export * from './CountdownWidget'
export * from './Dialogs/ConfirmDialog'
export * from './Dialogs/ConfirmationModal'
export * from './ExpandableVideo'
export * from './GlassPanel'
export * from './IconPanel'
export * from './InnerSideMenu'
export * from './LogsBarChart'
export * from './MobileSheetNav'
export * from './PrivacySettings'
export * from './PromoToast'
export * from './ThemeToggle'
export * from './TweetCard'
export * from './InnerSideMenu'
export * from './ShimmeringLoader'
export * from './Dialogs/ConfirmDialog'
export * from './Dialogs/ConfirmationModal'
export * from './AssistantChat/AssistantChatForm'
export * from './AssistantChat/AssistantCommandsPopover'
export * from './PromoToast'
export * from './admonition'
export * from './ComputeBadge'
export * from './TimestampInfo'
export * from './FilterBar'
export * from './Toc'
export * from './LogsBarChart'
export * from './ShimmeringLoader'

View File

@@ -14,6 +14,7 @@
"@hookform/resolvers": "^3.1.1",
"@monaco-editor/react": "^4.6.0",
"@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "catalog:",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
@@ -22,6 +23,7 @@
"dayjs": "^1.11.13",
"framer-motion": "^11.1.9",
"github-slugger": "^2.0.0",
"icons": "workspace:*",
"lodash": "^4.17.21",
"lucide-react": "*",
"mdast": "^3.0.0",

6
pnpm-lock.yaml generated
View File

@@ -2015,6 +2015,9 @@ importers:
'@supabase/sql-to-rest':
specifier: ^0.1.6
version: 0.1.6(encoding@0.1.13)(supports-color@8.1.1)
'@supabase/supabase-js':
specifier: 'catalog:'
version: 2.49.3
class-variance-authority:
specifier: ^0.6.0
version: 0.6.1
@@ -2039,6 +2042,9 @@ importers:
github-slugger:
specifier: ^2.0.0
version: 2.0.0
icons:
specifier: workspace:*
version: link:../icons
lodash:
specifier: ^4.17.21
version: 4.17.21

View File

@@ -1,436 +0,0 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
page: {
Row: {
checksum: string | null
id: number
last_refresh: string | null
meta: Json | null
parent_page_id: number | null
path: string
source: string | null
type: string | null
version: string | null
}
Insert: {
checksum?: string | null
id?: number
last_refresh?: string | null
meta?: Json | null
parent_page_id?: number | null
path: string
source?: string | null
type?: string | null
version?: string | null
}
Update: {
checksum?: string | null
id?: number
last_refresh?: string | null
meta?: Json | null
parent_page_id?: number | null
path?: string
source?: string | null
type?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "page_parent_page_id_fkey"
columns: ["parent_page_id"]
referencedRelation: "page"
referencedColumns: ["id"]
}
]
}
page_section: {
Row: {
content: string | null
embedding: string | null
fts_tokens: unknown | null
heading: string | null
id: number
page_id: number
slug: string | null
token_count: number | null
}
Insert: {
content?: string | null
embedding?: string | null
fts_tokens?: unknown | null
heading?: string | null
id?: number
page_id: number
slug?: string | null
token_count?: number | null
}
Update: {
content?: string | null
embedding?: string | null
fts_tokens?: unknown | null
heading?: string | null
id?: number
page_id?: number
slug?: string | null
token_count?: number | null
}
Relationships: [
{
foreignKeyName: "page_section_page_id_fkey"
columns: ["page_id"]
referencedRelation: "page"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
docs_search_embeddings: {
Args: {
embedding: string
match_threshold: number
}
Returns: {
id: number
path: string
type: string
title: string
subtitle: string
description: string
headings: string[]
slugs: string[]
}[]
}
docs_search_fts: {
Args: {
query: string
}
Returns: {
id: number
path: string
type: string
title: string
subtitle: string
description: string
headings: string[]
slugs: string[]
}[]
}
get_page_parents: {
Args: {
page_id: number
}
Returns: {
id: number
parent_page_id: number
path: string
meta: Json
}[]
}
hnswhandler: {
Args: {
"": unknown
}
Returns: unknown
}
ivfflathandler: {
Args: {
"": unknown
}
Returns: unknown
}
match_page_sections: {
Args: {
embedding: string
match_threshold: number
match_count: number
min_content_length: number
}
Returns: {
id: number
page_id: number
slug: string
heading: string
content: string
similarity: number
}[]
}
match_page_sections_v2: {
Args: {
embedding: string
match_threshold: number
min_content_length: number
}
Returns: {
content: string | null
embedding: string | null
fts_tokens: unknown | null
heading: string | null
id: number
page_id: number
slug: string | null
token_count: number | null
}[]
}
vector_avg: {
Args: {
"": number[]
}
Returns: string
}
vector_dims: {
Args: {
"": string
}
Returns: number
}
vector_norm: {
Args: {
"": string
}
Returns: number
}
vector_out: {
Args: {
"": string
}
Returns: unknown
}
vector_send: {
Args: {
"": string
}
Returns: string
}
vector_typmod_in: {
Args: {
"": unknown[]
}
Returns: number
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "buckets_owner_fkey"
columns: ["owner"]
referencedRelation: "users"
referencedColumns: ["id"]
}
]
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
referencedRelation: "buckets"
referencedColumns: ["id"]
}
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: unknown
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}