add pages to v2

This commit is contained in:
Francesco Sansalvadore committed 2026-03-27 11:39:16 +01:00
1 parent 60e1804b41
commit ef6e7b7e52
28 files changed
+472 -243

No files matched your search

@@ -0,0 +1,10 @@
import { redirect } from 'next/navigation'
export default async function ObsAdvisorsSecurityPage({
params,
}: {
params: Promise<{ projectRef: string }>
}) {
const { projectRef } = await params
redirect(`/v2/project/${projectRef}/advisors/security`)
}
@@ -0,0 +1,13 @@
'use client'
import { AdvisorLintsPageContent } from '@/components/interfaces/Advisors/AdvisorLintsPageContent'
export default function ObsAdvisorsPerformancePage() {
return (
<AdvisorLintsPageContent
category="PERFORMANCE"
title="Performance Advisor"
headerClassName="py-4 px-6 !mb-0"
/>
)
}
@@ -0,0 +1,7 @@
'use client'
import { AdvisorLintsPageContent } from '@/components/interfaces/Advisors/AdvisorLintsPageContent'
export default function ObsAdvisorsSecurityPage() {
return <AdvisorLintsPageContent category="SECURITY" title="Security Advisor" hideDbInspectCTA />
}
@@ -1,5 +1,5 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
import { redirect } from 'next/navigation'
export default function ObsAlertsPage() {
return <V2PlaceholderView title="Active alerts" />
redirect('./advisors/security')
}
@@ -1,5 +1,24 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams, useSearchParams } from 'next/navigation'
export default function ObsLogsApiPage() {
return <V2PlaceholderView title="API gateway logs" />
const params = useParams()
const searchParams = useSearchParams()
const projectRef = params?.projectRef as string | undefined
const identifier = searchParams?.get('db') ?? undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="api"
projectRef={projectRef}
tableName={LogsTableName.EDGE}
filterOverride={identifier ? { identifier } : undefined}
/>
)
}
@@ -1,5 +1,13 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
'use client'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsAuthPage() {
return <V2PlaceholderView title="Auth logs" />
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!projectRef) return null
return <LogsPreviewer condensedLayout projectRef={projectRef} queryType="auth" />
}
@@ -0,0 +1,21 @@
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsCronPage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="pg_cron"
projectRef={projectRef}
tableName={LogsTableName.PG_CRON}
/>
)
}
@@ -0,0 +1,22 @@
'use client'
import { IS_PLATFORM } from 'common'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsDedicatedPoolerPage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!IS_PLATFORM || !projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="pgbouncer"
projectRef={projectRef}
tableName={LogsTableName.PGBOUNCER}
/>
)
}
@@ -0,0 +1,21 @@
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsEdgeFunctionsPage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="fn_edge"
projectRef={projectRef}
tableName={LogsTableName.FN_EDGE}
/>
)
}
@@ -1,5 +1,5 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
import { redirect } from 'next/navigation'
export default function ObsLogsPage() {
return <V2PlaceholderView title="All logs" />
export default function ObsLogsIndexPage() {
redirect('./logs/api')
}
@@ -0,0 +1,22 @@
'use client'
import { IS_PLATFORM } from 'common'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsPoolerPage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!IS_PLATFORM || !projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="supavisor"
projectRef={projectRef}
tableName={LogsTableName.SUPAVISOR}
/>
)
}
@@ -1,5 +1,24 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams, useSearchParams } from 'next/navigation'
export default function ObsLogsPostgresPage() {
return <V2PlaceholderView title="Postgres logs" />
const params = useParams()
const searchParams = useSearchParams()
const projectRef = params?.projectRef as string | undefined
const identifier = searchParams?.get('db') ?? undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="database"
projectRef={projectRef}
tableName={LogsTableName.POSTGRES}
filterOverride={identifier ? { identifier } : undefined}
/>
)
}
@@ -0,0 +1,31 @@
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { LogsTableEmptyState } from '@/components/interfaces/Settings/Logs/LogsTableEmptyState'
import { useParams, useSearchParams } from 'next/navigation'
export default function ObsLogsPostgrestPage() {
const params = useParams()
const searchParams = useSearchParams()
const projectRef = params?.projectRef as string | undefined
const identifier = searchParams?.get('db') ?? undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="postgrest"
projectRef={projectRef}
tableName={LogsTableName.POSTGREST}
EmptyState={
<LogsTableEmptyState
title="No results found"
description="Only errors are captured into PostgREST logs by default. Check the API Gateway logs for HTTP requests."
/>
}
filterOverride={identifier ? { identifier } : undefined}
/>
)
}
@@ -0,0 +1,21 @@
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsRealtimePage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="realtime"
projectRef={projectRef}
tableName={LogsTableName.REALTIME}
/>
)
}
@@ -0,0 +1,21 @@
'use client'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import { useParams } from 'next/navigation'
export default function ObsLogsStoragePage() {
const params = useParams()
const projectRef = params?.projectRef as string | undefined
if (!projectRef) return null
return (
<LogsPreviewer
condensedLayout
queryType="storage"
projectRef={projectRef}
tableName={LogsTableName.STORAGE}
/>
)
}
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation'
export default function ObsLogsIndexPage() {
redirect('./obs/advisors/security')
}
@@ -1,5 +1,5 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
export default function SettingsApiKeysPage() {
return <V2PlaceholderView title="API keys" />
export default function SettingsPreferencesPage() {
return <V2PlaceholderView title="Preferences" />
}
@@ -0,0 +1,5 @@
import { V2PlaceholderView } from '@/components/v2/views/V2PlaceholderView'
export default function SettingsApiKeysPage() {
return <V2PlaceholderView title="API keys" />
}
@@ -0,0 +1,114 @@
'use client'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import { LinterPageFooter } from 'components/interfaces/Linter/LinterPageFooter'
import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useProjectLintsQuery, type Lint } from 'data/lint/lint-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { useMemo, useState } from 'react'
import { useRouter as useCompatRouter } from 'next/compat/router'
import { useSearchParams } from 'next/navigation'
import { LoadingLine } from 'ui'
type AdvisorCategory = 'SECURITY' | 'PERFORMANCE'
interface AdvisorLintsPageContentProps {
category: AdvisorCategory
title: string
hideDbInspectCTA?: boolean
headerClassName?: string
}
export function AdvisorLintsPageContent({
category,
title,
hideDbInspectCTA,
headerClassName,
}: AdvisorLintsPageContentProps) {
const compatRouter = useCompatRouter()
const searchParams = useSearchParams()
const preset = (compatRouter?.query?.preset as string | undefined) ?? searchParams?.get('preset') ?? undefined
const id = (compatRouter?.query?.id as string | undefined) ?? searchParams?.get('id') ?? undefined
const { data: project } = useSelectedProjectQuery()
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
{ level: LINTER_LEVELS.ERROR, filters: [] },
{ level: LINTER_LEVELS.WARN, filters: [] },
{ level: LINTER_LEVELS.INFO, filters: [] },
])
const [currentTab, setCurrentTab] = useState<LINTER_LEVELS>(
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
)
const {
data,
isPending: isLoading,
isRefetching,
refetch,
} = useProjectLintsQuery({
projectRef: project?.ref,
})
const activeLints = useMemo(() => {
return [...(data ?? [])].filter((x) => x.categories.includes(category))
}, [data, category])
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
[]) as string[]
const filteredLints = activeLints
.filter((x) => x.level === currentTab)
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
const filterOptions = lintInfoMap
.filter((item) => activeLints.some((lint) => lint.name === item.name && lint.level === currentTab))
.map((type) => ({
name: type.title,
value: type.name,
}))
const selectedLint: Lint | null = useMemo(() => {
return activeLints.find((lint) => lint.cache_key === id) ?? null
}, [id, activeLints])
return (
<div className="h-full flex flex-col">
<FormHeader
className={headerClassName ?? 'py-4 px-6 !-mb-px'}
title={title}
docsUrl={`${DOCS_URL}/guides/database/database-linter`}
/>
<LintPageTabs
activeLints={activeLints}
isLoading={isLoading}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<LinterFilters
filterOptions={filterOptions}
filteredLints={filteredLints}
activeLints={activeLints}
currentTab={currentTab}
filters={filters}
isLoading={isLoading || isRefetching}
setFilters={setFilters}
onClickRefresh={refetch}
/>
<LoadingLine loading={isRefetching} />
<LinterDataGrid
filteredLints={filteredLints}
currentTab={currentTab}
selectedLint={selectedLint}
isLoading={isLoading}
/>
<LinterPageFooter
hideDbInspectCTA={hideDbInspectCTA}
isLoading={isLoading}
isRefetching={isRefetching}
refetch={refetch}
/>
</div>
)
}
@@ -13,8 +13,9 @@ import {
} from 'ui'
import { LINTER_LEVELS, LINT_TABS } from 'components/interfaces/Linter/Linter.constants'
import { Lint } from 'data/lint/lint-query'
import { useRouter } from 'next/router'
import type { Lint } from 'data/lint/lint-query'
import { useRouter as useCompatRouter } from 'next/compat/router'
import { usePathname, useRouter as useAppRouter, useSearchParams } from 'next/navigation'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
interface LintPageTabsProps {
@@ -24,7 +25,10 @@ interface LintPageTabsProps {
activeLints: Lint[]
}
const LintPageTabs = ({ currentTab, setCurrentTab, isLoading, activeLints }: LintPageTabsProps) => {
const router = useRouter()
const compatRouter = useCompatRouter()
const appRouter = useAppRouter()
const pathname = usePathname() ?? ''
const searchParams = useSearchParams()
const warnLintsCount = activeLints.filter((x) => x.level === 'WARN').length
const errorLintsCount = activeLints.filter((x) => x.level === 'ERROR').length
@@ -66,8 +70,21 @@ const LintPageTabs = ({ currentTab, setCurrentTab, isLoading, activeLints }: Lin
defaultValue={currentTab}
onValueChange={(value) => {
setCurrentTab(value as LINTER_LEVELS)
const { sort, search, ...rest } = router.query
router.push({ ...router, query: { ...rest, preset: value, id: null } })
if (compatRouter) {
const { sort: _sort, search: _search, ...rest } = compatRouter.query
void compatRouter.push({
pathname: compatRouter.pathname ?? '/',
query: { ...rest, preset: value, id: null },
})
} else {
const next = new URLSearchParams(searchParams?.toString() ?? '')
next.delete('sort')
next.delete('search')
next.delete('id')
next.set('preset', value)
const qs = next.toString()
void appRouter.push(qs ? `${pathname}?${qs}` : pathname)
}
}}
>
<TabsList_Shadcn_ className={cn('flex gap-0 border-0 items-end z-10 relative')}>
@@ -1,17 +1,18 @@
import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import type { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import {
LintCategoryBadge,
LintEntity,
lintInfoMap,
NoIssuesFound,
} from 'components/interfaces/Linter/Linter.utils'
import { Lint } from 'data/lint/lint-query'
import type { Lint } from 'data/lint/lint-query'
import { useTrack } from 'lib/telemetry/track'
import { X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useRouter as useCompatRouter } from 'next/compat/router'
import { usePathname, useRouter as useAppRouter, useSearchParams } from 'next/navigation'
import { useRef } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import DataGrid, { type Column, type DataGridHandle, Row } from 'react-data-grid'
import ReactMarkdown from 'react-markdown'
import { Button, cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
@@ -34,7 +35,10 @@ const LinterDataGrid = ({
}: LinterDataGridProps) => {
const gridRef = useRef<DataGridHandle>(null)
const { ref } = useParams()
const router = useRouter()
const compatRouter = useCompatRouter()
const appRouter = useAppRouter()
const pathname = usePathname() ?? ''
const searchParams = useSearchParams()
const track = useTrack()
const lintCols = [
@@ -111,8 +115,15 @@ const LinterDataGrid = ({
})
function handleSidepanelClose() {
const { id, ...otherParams } = router.query
router.push({ query: otherParams })
if (compatRouter) {
const { id: _id, ...otherParams } = compatRouter.query
void compatRouter.push({ pathname: compatRouter.pathname ?? '/', query: otherParams })
} else {
const next = new URLSearchParams(searchParams?.toString() ?? '')
next.delete('id')
const qs = next.toString()
void appRouter.push(qs ? `${pathname}?${qs}` : pathname)
}
}
return (
@@ -148,8 +159,18 @@ const LinterDataGrid = ({
onClick={() => {
if (typeof idx === 'number' && idx >= 0) {
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
const { id, ...rest } = router.query
router.push({ ...router, query: { ...rest, id: props.row.cache_key } })
if (compatRouter) {
const { id: _id, ...rest } = compatRouter.query
void compatRouter.push({
pathname: compatRouter.pathname ?? '/',
query: { ...rest, id: props.row.cache_key },
})
} else {
const next = new URLSearchParams(searchParams?.toString() ?? '')
next.set('id', props.row.cache_key)
const qs = next.toString()
void appRouter.push(qs ? `${pathname}?${qs}` : pathname)
}
track('advisor_detail_opened', {
origin: 'advisors_page',
@@ -192,7 +213,7 @@ const LinterDataGrid = ({
<Button type="text" icon={<X />} onClick={handleSidepanelClose} />
</div>
<div className="p-6 flex-grow min-h-0 overflow-y-auto">
<LintDetail lint={selectedLint} projectRef={ref!} />
<LintDetail lint={selectedLint} projectRef={ref ?? ''} />
</div>
</ResizablePanel>
</>
+1 -1
View File
@@ -158,7 +158,7 @@ export function LeftActivityBar() {
icon: <ChartArea className="h-4 w-4" strokeWidth={1.5} />,
label: 'Observe',
badge: hasAdvisorWarnings,
href: `${base}/obs/logs`,
href: `${base}/obs/advisors/security`,
},
{
id: 'settings',
+22 -17
View File
@@ -5,10 +5,10 @@ import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import {
Button,
cn,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
cn,
Tooltip,
TooltipContent,
TooltipTrigger,
@@ -61,21 +61,19 @@ const DATA_GROUPS = [
const OBS_GROUPS = [
{
id: 'obs-logs',
label: 'Logs',
id: 'obs-alerts',
label: 'Advisors',
items: [
{ href: 'logs', label: 'All logs' },
{ href: 'logs/api', label: 'API gateway' },
{ href: 'logs/postgres', label: 'Postgres' },
{ href: 'logs/auth', label: 'Auth' },
{ href: 'advisors/security', label: 'Security' },
{ href: 'advisors/performance', label: 'Performance' },
],
},
{
id: 'obs-metrics',
label: 'Observe',
label: 'Observability',
items: [
{ href: 'metrics/api-overview', label: 'API gateway' },
{ href: 'metrics/query-performance', label: 'Query Performance' },
{ href: 'metrics/connections', label: 'API gateway' },
{ href: 'metrics/database', label: 'Database' },
{ href: 'metrics/postgrest', label: 'Data API' },
{ href: 'metrics/auth', label: 'Auth' },
@@ -85,11 +83,19 @@ const OBS_GROUPS = [
],
},
{
id: 'obs-alerts',
label: 'Advisors',
id: 'obs-logs',
label: 'Logs',
items: [
{ href: 'alerts', label: 'Security' },
{ href: 'alerts', label: 'Performance' },
{ href: 'logs/api', label: 'API gateway' },
{ href: 'logs/postgres', label: 'Postgres' },
{ href: 'logs/postgrest', label: 'PostgREST' },
{ href: 'logs/pooler', label: 'Pooler' },
{ href: 'logs/dedicated-pooler', label: 'Dedicated Pooler' },
{ href: 'logs/auth', label: 'Auth' },
{ href: 'logs/storage', label: 'Storage' },
{ href: 'logs/realtime', label: 'Realtime' },
{ href: 'logs/edge-functions', label: 'Edge Functions' },
{ href: 'logs/cron', label: 'Cron' },
],
},
]
@@ -103,6 +109,7 @@ const SETTINGS_GROUPS = [
{ href: 'compute', label: 'Compute & disk' },
{ href: 'network', label: 'Network' },
{ href: 'api-keys', label: 'API keys' },
{ href: 'preferences', label: 'Preferences' },
],
},
{
@@ -255,9 +262,7 @@ export function BrowserPanel({ onCollapse }: { onCollapse?: () => void }) {
</span>
</button>
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_>
{renderedItems}
</CollapsibleContent_Shadcn_>
<CollapsibleContent_Shadcn_>{renderedItems}</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
)
})}
+5 -1
View File
@@ -1,5 +1,6 @@
'use client'
import { FeedbackDropdown } from 'components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown'
import { cn } from 'ui'
import { CommandMenuTriggerInput } from 'ui-patterns'
@@ -12,7 +13,7 @@ export function TopBar() {
'h-11 md:h-12 flex items-center justify-between pl-1 pr-1.5 border-b border-border bg-dash-sidebar shrink-0'
)}
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0 shrink-0">
<V2ProjectBranchSelector />
</div>
<div className="flex items-center gap-2 shrink-0">
@@ -21,6 +22,9 @@ export function TopBar() {
className="text-foreground-lighter border border-border rounded px-2 py-1 text-xs"
/>
</div>
<div className="hidden md:block shrink-0">
<FeedbackDropdown />
</div>
</header>
)
}
@@ -1,27 +1,26 @@
'use client'
import { useMemo } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useMemo } from 'react'
import type { NextPageWithLayout } from 'types'
import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { useV2Params } from '@/app/v2/V2ParamsContext'
import { BannerStackProvider } from '@/components/ui/BannerStack/BannerStackProvider'
import { StudioDataWorkspace } from '@/components/v2/data/StudioDataWorkspace'
import UrlConfigurationPage from '@/pages/project/[ref]/auth/url-configuration'
import ProtectionPage from '@/pages/project/[ref]/auth/protection'
import AuditLogsPage from '@/pages/project/[ref]/auth/audit-logs'
import HooksPage from '@/pages/project/[ref]/auth/hooks'
import MfaPage from '@/pages/project/[ref]/auth/mfa'
import SessionsPage from '@/pages/project/[ref]/auth/sessions'
import OAuthServerPage from '@/pages/project/[ref]/auth/oauth-server'
import PerformancePage from '@/pages/project/[ref]/auth/performance'
import AuthPoliciesPage from '@/pages/project/[ref]/auth/policies'
import ProtectionPage from '@/pages/project/[ref]/auth/protection'
import RateLimitsPage from '@/pages/project/[ref]/auth/rate-limits'
import SessionsPage from '@/pages/project/[ref]/auth/sessions'
import SmtpPage from '@/pages/project/[ref]/auth/smtp'
import TemplatesPage from '@/pages/project/[ref]/auth/templates'
import AuthPoliciesPage from '@/pages/project/[ref]/auth/policies'
import HooksPage from '@/pages/project/[ref]/auth/hooks'
import ThirdPartyPage from '@/pages/project/[ref]/auth/third-party'
import OAuthServerPage from '@/pages/project/[ref]/auth/oauth-server'
import AuditLogsPage from '@/pages/project/[ref]/auth/audit-logs'
import PerformancePage from '@/pages/project/[ref]/auth/performance'
import type { NextPageWithLayout } from 'types'
import UrlConfigurationPage from '@/pages/project/[ref]/auth/url-configuration'
const TABS: Array<{ key: string; label: string; content: NextPageWithLayout }> = [
{ key: 'url', label: 'URL', content: UrlConfigurationPage },
@@ -64,7 +63,7 @@ export function V2AuthSettingsView() {
<StudioDataWorkspace projectRef={projectRef}>
<div className="flex h-full min-h-0 flex-col">
<Tabs_Shadcn_ value={activeTab} onValueChange={onTabChange}>
<div className="sticky top-0 z-10 border-b border-border bg-background px-4 shrink-0">
<div className="sticky top-0 z-10 bg-background px-4 shrink-0">
<TabsList_Shadcn_ className="h-auto bg-transparent p-0 gap-0 overflow-x-auto">
{TABS.map((tab) => (
<TabsTrigger_Shadcn_
@@ -1,96 +1,15 @@
import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import { LinterPageFooter } from 'components/interfaces/Linter/LinterPageFooter'
import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import { AdvisorLintsPageContent } from 'components/interfaces/Advisors/AdvisorLintsPageContent'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { useMemo, useState } from 'react'
import type { NextPageWithLayout } from 'types'
import { LoadingLine } from 'ui'
const ProjectLints: NextPageWithLayout = () => {
const { preset, id } = useParams()
const { data: project } = useSelectedProjectQuery()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
{ level: LINTER_LEVELS.ERROR, filters: [] },
{ level: LINTER_LEVELS.WARN, filters: [] },
{ level: LINTER_LEVELS.INFO, filters: [] },
])
const [currentTab, setCurrentTab] = useState<LINTER_LEVELS>(
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
)
const {
data,
isPending: isLoading,
isRefetching,
refetch,
} = useProjectLintsQuery({
projectRef: project?.ref,
})
const activeLints = useMemo(() => {
return [...(data ?? [])]?.filter((x) => x.categories.includes('PERFORMANCE'))
}, [data])
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
[]) as string[]
const filteredLints = activeLints
.filter((x) => x.level === currentTab)
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
const filterOptions = lintInfoMap
// only show filters for lint types which are present in the results and not ignored
.filter((item) =>
activeLints.some((lint) => lint.name === item.name && lint.level === currentTab)
)
.map((type) => ({
name: type.title,
value: type.name,
}))
const selectedLint: Lint | null = useMemo(() => {
return activeLints.find((lint) => lint.cache_key === id) ?? null
}, [id, activeLints])
return (
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !mb-0"
title="Performance Advisor"
docsUrl={`${DOCS_URL}/guides/database/database-linter`}
/>
<LintPageTabs
activeLints={activeLints}
isLoading={isLoading}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<LinterFilters
filterOptions={filterOptions}
activeLints={activeLints}
filteredLints={filteredLints}
currentTab={currentTab}
filters={filters}
isLoading={isLoading || isRefetching}
setFilters={setFilters}
onClickRefresh={refetch}
/>
<LoadingLine loading={isRefetching} />
<LinterDataGrid
filteredLints={filteredLints}
currentTab={currentTab}
selectedLint={selectedLint}
isLoading={isLoading}
/>
<LinterPageFooter isLoading={isLoading} isRefetching={isRefetching} refetch={refetch} />
</div>
<AdvisorLintsPageContent
category="PERFORMANCE"
title="Performance Advisor"
headerClassName="py-4 px-6 !mb-0"
/>
)
}
@@ -1,105 +1,10 @@
import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
import { LinterPageFooter } from 'components/interfaces/Linter/LinterPageFooter'
import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
import { AdvisorLintsPageContent } from 'components/interfaces/Advisors/AdvisorLintsPageContent'
import AdvisorsLayout from 'components/layouts/AdvisorsLayout/AdvisorsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { useMemo, useState } from 'react'
import type { NextPageWithLayout } from 'types'
import { LoadingLine } from 'ui'
const ProjectLints: NextPageWithLayout = () => {
const { preset, id } = useParams()
const { data: project } = useSelectedProjectQuery()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
{ level: LINTER_LEVELS.ERROR, filters: [] },
{ level: LINTER_LEVELS.WARN, filters: [] },
{ level: LINTER_LEVELS.INFO, filters: [] },
])
const [currentTab, setCurrentTab] = useState<LINTER_LEVELS>(
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
)
const {
data,
isPending: isLoading,
isRefetching,
refetch,
} = useProjectLintsQuery({
projectRef: project?.ref,
})
const activeLints = (data ?? []).filter((lint) => lint.categories.includes('SECURITY'))
// hide vulnerable_postgres_version lint temporarily
// https://linear.app/supabase/project/pg-minor-version-upgrade-for-security-vulnerabilities-0124b2c2dcf5
// https://github.com/supabase/supabase/pull/38280/files
//.filter((lint) => lint.name !== 'vulnerable_postgres_version')
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
[]) as string[]
const filteredLints = activeLints
.filter((x) => x.level === currentTab)
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
const filterOptions = lintInfoMap
// only show filters for lint types which are present in the results and not ignored
.filter((item) =>
activeLints.some((lint) => lint.name === item.name && lint.level === currentTab)
)
.map((type) => ({
name: type.title,
value: type.name,
}))
const selectedLint: Lint | null = useMemo(() => {
return activeLints.find((lint) => lint.cache_key === id) ?? null
}, [id, activeLints])
return (
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !-mb-px"
title="Security Advisor"
docsUrl={`${DOCS_URL}/guides/database/database-linter`}
/>
<LintPageTabs
activeLints={activeLints}
isLoading={isLoading}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<LinterFilters
filterOptions={filterOptions}
filteredLints={filteredLints}
activeLints={activeLints}
currentTab={currentTab}
filters={filters}
isLoading={isLoading || isRefetching}
setFilters={setFilters}
onClickRefresh={refetch}
/>
<LoadingLine loading={isRefetching} />
<LinterDataGrid
filteredLints={filteredLints}
currentTab={currentTab}
selectedLint={selectedLint}
isLoading={isLoading}
/>
<LinterPageFooter
hideDbInspectCTA
isLoading={isLoading}
isRefetching={isRefetching}
refetch={refetch}
/>
</div>
)
return <AdvisorLintsPageContent category="SECURITY" title="Security Advisor" hideDbInspectCTA />
}
ProjectLints.getLayout = (page) => (