mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
add pages to v2
This commit is contained in:
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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_>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in new issue
Block a user