Chore/linter v2 (#22790)

* Start mocking in v2 elements

* wip

* Cleanup header

* Get side panel working

* Style side panel

* Cleanup

* Add new lint

* Start filters

* Add lint filters

* Nudge loading state

* Fix empty state

* Cleanup

* Cleanup

* Font nudges

* Visual nudges

* Fix filters

* Fix shimmer

* Cleanup
This commit is contained in:
Terry Sutton authored and GitHub committed 2024-04-17 12:42:54 -02:30
1 parent 34d152ce78
commit 6b1a44524c
4 files changed
+551 -394

No files matched your search

@@ -1,4 +1,5 @@
import { LINT_TYPES, Lint } from 'data/lint/lint-query'
import { AlertCircle, Box, Eye, Key, Lock, Table2, Unlock } from 'lucide-react'
import Link from 'next/link'
import { Button } from 'ui'
@@ -6,40 +7,79 @@ const assertUnreachable = (n: never) => {
console.error('Unhandled lint type', n)
}
export function getHumanReadableTitle(title: LINT_TYPES) {
switch (title) {
case 'unindexed_foreign_keys':
return 'Unindexed foreign keys'
case 'auth_users_exposed':
return 'Exposed Auth Users'
case 'auth_rls_initplan':
return 'Auth RLS Initialization Plan'
case 'no_primary_key':
return 'No Primary Key'
case 'unused_index':
return 'Unused Index'
case 'multiple_permissive_policies':
return 'Multiple Permissive Policies'
case 'function_search_path_mutable':
return 'Function Search Path Mutable'
case 'rls_enabled_no_policy':
return 'RLS Enabled No Policy'
case 'policy_exists_rls_disabled':
return 'Policy Exists RLS Disabled'
case 'rls_disabled_in_public':
return 'RLS Disabled in Public'
case 'security_definer_view':
return 'Security Definer View'
case 'duplicate_index':
return 'Duplicate Index'
case 'extension_in_public':
return 'Extension in Public'
default:
assertUnreachable(title)
throw new Error('This case should never be reached')
}
interface LintInfo {
title: string
icon: JSX.Element
}
export const lintInfoMap = [
{
name: 'unindexed_foreign_keys',
title: 'Unindexed foreign keys',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'auth_users_exposed',
title: 'Exposed Auth Users',
icon: <Lock className="text-foreground-muted" size={15} strokeWidth={1.5} />,
},
{
name: 'auth_rls_initplan',
title: 'Auth RLS Initialization Plan',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'no_primary_key',
title: 'No Primary Key',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'unused_index',
title: 'Unused Index',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'multiple_permissive_policies',
title: 'Multiple Permissive Policies',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'function_search_path_mutable',
title: 'Function Search Path Mutable',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'rls_enabled_no_policy',
title: 'RLS Enabled No Policy',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'policy_exists_rls_disabled',
title: 'Policy Exists RLS Disabled',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'rls_disabled_in_public',
title: 'RLS Disabled in Public',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'security_definer_view',
title: 'Security Definer View',
icon: <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />,
},
{
name: 'duplicate_index',
title: 'Duplicate Index',
icon: <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />,
},
{
name: 'extension_in_public',
title: 'Extension in Public',
icon: <Unlock className="text-foreground-muted" size={15} strokeWidth={1} />,
},
]
export const LintCTA = ({
title,
projectRef,
@@ -57,6 +97,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/database/indexes?schema=${metadata?.schema}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
Create an index
</Link>
@@ -66,7 +107,12 @@ export const LintCTA = ({
case 'auth_users_exposed':
return (
<Button asChild type="default">
<Link href={`/project/${projectRef}/editor`} target="_blank" rel="noreferrer">
<Link
href={`/project/${projectRef}/editor`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View table
</Link>
</Button>
@@ -74,7 +120,12 @@ export const LintCTA = ({
case 'auth_rls_initplan':
return (
<Button asChild type="default">
<Link href={`/project/${projectRef}/auth/policies`} target="_blank" rel="noreferrer">
<Link
href={`/project/${projectRef}/auth/policies`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View policies
</Link>
</Button>
@@ -82,7 +133,12 @@ export const LintCTA = ({
case 'no_primary_key':
return (
<Button asChild type="default">
<Link href={`/project/${projectRef}/editor`} target="_blank" rel="noreferrer">
<Link
href={`/project/${projectRef}/editor`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View table
</Link>
</Button>
@@ -94,6 +150,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/database/indexes?schema=${metadata?.schema}&table=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View index
</Link>
@@ -106,6 +163,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/database/indexes?schema=${metadata?.schema}&table=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View index
</Link>
@@ -118,6 +176,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/auth/policies?schema=${metadata?.schema}&search=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View policies
</Link>
@@ -130,6 +189,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/database/functions?schema=${metadata?.schema}&search=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View functions
</Link>
@@ -142,6 +202,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/auth/policies?schema=${metadata?.schema}&search=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View policies
</Link>
@@ -154,6 +215,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/auth/policies?schema=${metadata?.schema}&search=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View policies
</Link>
@@ -166,6 +228,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/auth/policies?schema=${metadata?.schema}&search=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View policies
</Link>
@@ -181,6 +244,7 @@ export const LintCTA = ({
href={`/project/${projectRef}/database/extensions?filter=${metadata?.name}`}
target="_blank"
rel="noreferrer"
className="no-underline"
>
View extension
</Link>
@@ -191,3 +255,14 @@ export const LintCTA = ({
return <></>
}
}
export const entityTypeIcon = (type: string) => {
switch (type) {
case 'table':
return <Table2 className="text-foreground-muted" size={15} strokeWidth={1} />
case 'view':
return <Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />
default:
return <Box className="text-foreground-muted" size={15} strokeWidth={1.5} />
}
}
@@ -1,162 +0,0 @@
import { Eye, HelpCircle, Table2 } from 'lucide-react'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import { LINT_TYPES, Lint } from 'data/lint/lint-query'
import { Badge, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
import { Markdown } from '../Markdown'
import { LintCTA, getHumanReadableTitle } from './ReportLints.utils'
type ReportLintsTableRowProps = {
lint: Lint
}
const ReportLintsTableRow = ({ lint }: ReportLintsTableRowProps) => {
const { ref } = useParams()
// TODO: Comment out functionality for ignoring lints because it relied on local storage. Will revisit later.
// const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
// const [lintIgnoreList, setLintIgnoreList] = useLocalStorageQuery<string[]>(
// LOCAL_STORAGE_KEYS.PROJECT_LINT_IGNORE_LIST,
// []
// )
// const isIgnored = lintIgnoreList.includes(lint.cache_key)
// if the lint type can't be handled (there's no CTA text defined), don't render it
if (!LINT_TYPES.includes(lint.name)) {
console.log('Unhandled lint type:', lint.name)
return null
}
// const toggleLintIgnore = () => {
// let currentIgnoreList = []
// const cacheKey = lint.cache_key
// // Check if the cacheKey exists in the array and ignore or unignore it
// const index = lintIgnoreList.indexOf(cacheKey)
// if (index !== -1) {
// currentIgnoreList = lintIgnoreList.filter((l) => l !== cacheKey)
// } else {
// currentIgnoreList = lintIgnoreList.concat(cacheKey)
// }
// setLintIgnoreList(currentIgnoreList)
// setSelectedLint(null)
// }
return (
<>
<Table.tr>
<Table.td className="w-20 align-top">
<Badge
className="!rounded w-16 font-mono text-center justify-center"
variant={
lint.level === 'ERROR' ? 'destructive' : lint.level === 'WARN' ? 'warning' : 'default'
}
>
{lint.level}
</Badge>
</Table.td>
<Table.td className="flex flex-col gap-y-2">
<div>
<div className="flex items-center gap-x-2">
<p className="text-foreground">{getHumanReadableTitle(lint.name)}</p>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<HelpCircle size={14} />
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="w-72">
{lint.description}
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</div>
{(lint.metadata?.type === 'table' || lint.metadata?.type === 'view') && (
<div className="flex items-center gap-x-4 text-xs py-1">
{lint.metadata?.schema && (
<div className="flex items-center gap-x-1">
<p className="text-foreground-lighter">schema</p>
<p className="text-foreground font-mono">{lint.metadata?.schema}</p>
</div>
)}
<div className="flex items-center gap-x-1">
{lint.metadata?.type === 'table' && (
<Table2 size={15} strokeWidth={1.5} className="text-foreground-lighter" />
)}
{lint.metadata?.type === 'view' && (
<Eye size={15} strokeWidth={1.5} className="text-foreground-lighter" />
)}
<p className="text-foreground font-mono">{lint.metadata?.name}</p>
</div>
</div>
)}
</div>
<div className="flex flex-col gap-y-1">
{lint.detail && (
<Markdown
className="text-foreground-light max-w-full leading-relaxed"
content={lint.detail}
/>
)}
{lint.remediation && (
<p className="text-foreground-light max-w-full leading-relaxed">
You can read more about this lint rule and ways to remedy it{' '}
<a
className="underline text-foreground-light transition-all hover:text-foreground hover:decoration-brand"
href={lint.remediation}
target="_blank"
rel="noreferrer"
>
here
</a>
</p>
)}
</div>
</Table.td>
<Table.td>
<div className="flex items-center justify-end gap-x-2">
<LintCTA title={lint.name} projectRef={ref!} metadata={lint.metadata} />
{/* <Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="text"
size="small"
className="px-1"
onClick={(event) => {
event.stopPropagation()
setSelectedLint(lint)
}}
icon={isIgnored ? <EyeIcon strokeWidth={1} /> : <EyeOff strokeWidth={1} />}
/>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">
{isIgnored ? 'Unignore problem' : 'Ignore problem'}
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_> */}
</div>
</Table.td>
</Table.tr>
{/* <Modal
size="small"
alignFooter="right"
visible={selectedLint !== null}
onCancel={() => setSelectedLint(null)}
onConfirm={() => toggleLintIgnore()}
header={<h3>Confirm to {isIgnored ? 'unignore' : 'ignore'} this lint</h3>}
>
<div className="py-4">
<Modal.Content>
<p className="text-sm">
{isIgnored
? 'Unignoring this lint will remove it from the Ignored Issues list. It will move it back to the main list above.'
: 'Ignoring this lint will remove it from the main list. It will still be visible in the Ignored Issues list below.'}
</p>
</Modal.Content>
</div>
</Modal> */}
</>
)
}
export default ReportLintsTableRow
+5 -4
View File
@@ -126,7 +126,7 @@ where
-- Standard View, accessible to anon or authenticated that is security_definer
(
c.relkind = 'v' -- v for view
-- Exclude security invoker views
-- Exclude security invoker views
and not (
lower(coalesce(c.reloptions::text,'{}'))::text[]
&& array[
@@ -141,7 +141,7 @@ where
-- Standard View, security invoker, but no RLS enabled on auth.users
(
c.relkind in ('v') -- v for view
-- is security invoker
-- is security invoker
and (
lower(coalesce(c.reloptions::text,'{}'))::text[]
&& array[
@@ -151,7 +151,7 @@ where
'security_invoker=on'
]
)
and not pg_class_auth_users.relrowsecurity
and not pg_class_auth_users.relrowsecurity
)
)
group by
@@ -663,7 +663,7 @@ where
and not c.relrowsecurity)
union all
(
select
select
'extension_in_public' as name,
'WARN' as level,
'EXTERNAL' as facing,
@@ -706,6 +706,7 @@ export const LINT_TYPES = [
'rls_disabled_in_public',
'extension_in_public',
] as const
export type LINT_TYPES = (typeof LINT_TYPES)[number]
export type Lint = {
@@ -1,224 +1,467 @@
import { sortBy } from 'lodash'
import { Check, ExternalLink, Loader } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Eye, MessageSquareMore, Table2, TextSearch, X } from 'lucide-react'
import { useRef, useState } from 'react'
import { getHumanReadableTitle } from 'components/interfaces/Reports/ReportLints.utils'
import ReportLintsTableRow from 'components/interfaces/Reports/ReportLintsTableRow'
import { InformationCircleIcon } from '@heroicons/react/16/solid'
import { useParams } from 'common'
import { lintInfoMap } from 'components/interfaces/Reports/ReportLints.utils'
import { DatabaseLayout } from 'components/layouts'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import Table from 'components/to-be-cleaned/Table'
import { FilterPopover } from 'components/ui/FilterPopover'
import { FormHeader } from 'components/ui/Forms'
import { LINT_TYPES, useProjectLintsQuery } from 'data/lint/lint-query'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useSelectedProject } from 'hooks'
import { useRouter } from 'next/router'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import type { NextPageWithLayout } from 'types'
import { Button, LoadingLine } from 'ui'
import {
Button,
LoadingLine,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
Tabs_Shadcn_,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
cn,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { FilterPopover } from 'components/ui/FilterPopover'
import ReactMarkdown from 'react-markdown'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { Markdown } from '../../../../components/interfaces/Markdown'
import {
LintCTA,
entityTypeIcon,
} from '../../../../components/interfaces/Reports/ReportLints.utils'
enum LINTER_LEVELS {
ERROR = 'ERROR',
WARN = 'WARN',
INFO = 'INFO',
}
const LINT_TABS = [
{
id: LINTER_LEVELS.ERROR,
label: 'Errors',
description: 'You should consider these issues urgent and and fix them as soon as you can.',
},
{
id: LINTER_LEVELS.WARN,
label: 'Warnings ',
description: 'You should try and read through these issues and fix them if necessary.',
},
{
id: LINTER_LEVELS.INFO,
label: 'Info ',
description: 'You should read through these suggestions and consider implementing them.',
},
]
const ProjectLints: NextPageWithLayout = () => {
const project = useSelectedProject()
const [filters, setFilters] = useState({
levels: [] as string[],
types: [] as string[],
})
// const [lintIgnoreList] = useLocalStorageQuery<string[]>(
// LOCAL_STORAGE_KEYS.PROJECT_LINT_IGNORE_LIST,
// []
// )
const router = useRouter()
const { ref } = useParams()
const gridRef = useRef<DataGridHandle>(null)
const { preset } = useParams()
// need to maintain a list of filters for each tab
const [filters, setFilters] = useState([
{ 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 [selectedRow, setSelectedRow] = useState<number>()
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
const [view, setView] = useState<'details' | 'suggestion'>('details')
const { data, isLoading, isRefetching, refetch } = useProjectLintsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
// sort the lints by level, ERROR should be at the top.
const lints = sortBy(data || [], (lint) => {
if (lint.level === 'ERROR') return 0
if (lint.level === 'WARN') return 1
if (lint.level === 'INFO') return 2
return 3
})
const activeLints = data || []
const activeLints = lints
// const [ignoredLints, activeLints] = partition(lints, (lint) =>
// lintIgnoreList.includes(lint.cache_key)
// )
const filteredLints = useMemo(() => {
return activeLints
.filter((x) => (filters.levels.length > 0 ? filters.levels.includes(x.level) : x))
.filter((x) => (filters.types.length > 0 ? filters.types.includes(x.name) : x))
}, [activeLints, filters.levels, filters.types])
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 warnLintsCount = activeLints.filter((x) => x.level === 'WARN').length
const errorLintsCount = activeLints.filter((x) => x.level === 'ERROR').length
const infoLintsCount = activeLints.filter((x) => x.level === 'INFO').length
const filterOptions = useMemo(() => {
const filterOptions = lintInfoMap
// only show filters for lint types which are present in the results and not ignored
return LINT_TYPES.filter((type) => activeLints.some((lint) => lint.name === type)).map(
(type) => ({
name: getHumanReadableTitle(type),
value: type,
})
.filter((item) =>
activeLints.some((lint) => lint.name === item.name && lint.level === currentTab)
)
}, [activeLints])
.map((type) => ({
name: type.title,
value: type.name,
}))
const updateFilters = (level: any, newFilters: any) => {
// Create a copy of the current filters state
const updatedFilters = [...filters]
// Find the index of the filter object corresponding to the provided level
const index = updatedFilters.findIndex((filter) => filter.level === level)
if (index !== -1) {
// Update the filters array at the found index with the new filters
updatedFilters[index] = { ...updatedFilters[index], filters: newFilters }
// Update the filters state with the updated array
setFilters(updatedFilters)
}
}
const lintCountLabel = (count: number, label: string) => (
<>
{isLoading ? (
<ShimmeringLoader className="w-20 pt-1" />
) : (
<>
{count} {label}
</>
)}
</>
)
const lintCols = [
{
id: 'name',
name: 'Issue type',
description: undefined,
minWidth: 240,
value: (row: any) => (
<div className="flex items-center gap-1.5">
<span className="shrink-0">
{lintInfoMap.find((item) => row.name === item.name)?.icon}
</span>
{<h3 className="text-xs">{lintInfoMap.find((item) => row.name === item.name)?.title}</h3>}
</div>
),
},
{
id: 'metadata.name',
name: 'Entity/item',
description: undefined,
minWidth: 230,
value: (row: any) => (
<div className="flex items-center gap-1 text-xs">
<span className="shrink-0">{entityTypeIcon(row.metadata?.type)}</span>
{`${row.metadata.schema}.${row.metadata.name}`}
</div>
),
},
{
id: 'description',
name: 'Description',
description: undefined,
minWidth: 400,
value: (row: any) => <ReactMarkdown className="text-xs">{row.description}</ReactMarkdown>,
},
]
const columns = lintCols.map((col) => {
const result: Column<any> = {
key: col.id,
name: col.name,
resizable: true,
minWidth: col.minWidth ?? 120,
headerCellClass: 'first:pl-6 cursor-pointer',
renderHeaderCell: () => {
return (
<div className="flex items-center justify-between font-mono font-normal text-xs w-full">
<div className="flex items-center gap-x-2">
<p className="!text-foreground">{col.name}</p>
{col.description && <p className="text-foreground-lighter">{col.description}</p>}
</div>
</div>
)
},
renderCell: (props) => {
const value = col.value(props.row)
return (
<div
className={cn(
'w-full flex flex-col justify-center font-mono text-xs',
typeof value === 'number' ? 'text-right' : ''
)}
>
<span>{value}</span>
</div>
)
},
}
return result
})
return (
<ScaffoldContainer>
<ScaffoldSection>
<div className="col-span-12">
<FormHeader
className="!mb-0"
title="Database Linter"
description="Identify common schema problems in your database."
/>
</div>
<div className="col-span-12 flex items-center justify-between">
<div className="flex items-center gap-x-4">
<div className="flex items-center gap-x-2">
<p className="text-xs prose">Filter by</p>
<FilterPopover
name="Level"
options={[
{ name: 'Info', value: 'INFO' },
{ name: 'Warning', value: 'WARN' },
{ name: 'Error', value: 'ERROR' },
]}
labelKey="name"
valueKey="value"
activeOptions={filters.levels}
onSaveFilters={(values) => setFilters({ ...filters, levels: values })}
/>
<FilterPopover
name="Type"
options={filterOptions}
labelKey="name"
valueKey="value"
activeOptions={filters.types}
onSaveFilters={(values) => setFilters({ ...filters, types: values })}
/>
</div>
<p className="text-foreground-light text-xs">
Identified {activeLints.length} problems{' '}
{warnLintsCount > 0 || errorLintsCount > 0
? `(${errorLintsCount > 0 ? `${errorLintsCount} errors` : ''}${errorLintsCount > 0 && warnLintsCount > 0 ? ', ' : ''}${warnLintsCount > 0 ? `${warnLintsCount} warnings` : ''})`
: null}
</p>
</div>
<div className="flex items-center gap-x-2">
<Button asChild type="default" icon={<ExternalLink />}>
<a href="https://supabase.github.io/splinter" target="_blank" rel="noreferrer">
Documentation
</a>
</Button>
<Button
type="primary"
disabled={isLoading || isRefetching}
loading={isLoading || isRefetching}
onClick={() => refetch()}
<div className="h-full flex flex-col">
<FormHeader
className="py-4 px-6 !mb-0"
title="Suggestions"
docsUrl="https://supabase.github.io/splinter/"
/>
<Tabs_Shadcn_
defaultValue={currentTab}
onValueChange={(value) => {
setCurrentTab(value as LINTER_LEVELS)
setSelectedLint(null)
setSelectedRow(undefined)
const { sort, search, ...rest } = router.query
router.push({ ...router, query: { ...rest, preset: value } })
}}
>
<TabsList_Shadcn_ className={cn('flex gap-0 border-0 items-end z-10 relative')}>
{LINT_TABS.map((tab) => (
<TabsTrigger_Shadcn_
key={tab.id}
value={tab.id}
className={cn(
'group relative',
'px-6 py-3 border-b-0 flex flex-col items-start !shadow-none border-default border-t',
'even:border-x last:border-r even:!border-x-strong last:!border-r-strong',
tab.id === currentTab ? '!bg-surface-200' : '!bg-surface-200/[33%]',
'hover:!bg-surface-100',
'data-[state=active]:!bg-surface-200',
'hover:text-foreground-light',
'transition'
)}
>
Rerun linter
</Button>
</div>
</div>
{tab.id === currentTab && (
<div className="absolute top-0 left-0 w-full h-[1px] bg-foreground" />
)}
<div className="flex items-center gap-x-2">
<span
className={
tab.id === LINTER_LEVELS.ERROR
? 'text-destructive-600'
: tab.id === LINTER_LEVELS.WARN
? 'text-warning-600'
: 'text-brand-500'
}
>
<MessageSquareMore size={14} fill="currentColor" strokeWidth={0} />
</span>
<div className="col-span-12">
<Table
head={[
<Table.th key="level" className="py-2">
Level
</Table.th>,
<Table.th key="header-type" className="py-2">
Problem
</Table.th>,
<Table.th key="header-expand" className="py-2 text-right"></Table.th>,
]}
body={[
<Table.tr key="loader">
<Table.td colSpan={12} className="!p-0">
<LoadingLine loading={isLoading || isRefetching} />
</Table.td>
</Table.tr>,
...(activeLints.length === 0
? [
<Table.tr key="empty-state">
<Table.td colSpan={6} className="p-3 py-12">
{isLoading ? (
<div className="flex items-center gap-x-2">
<Loader className="animate-spin" size={12} />
<p className="text-foreground-light">Checking database for issues...</p>
<span className="">{tab.label}</span>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<InformationCircleIcon className="transition text-foreground-muted w-3 h-3 data-[state=delayed-open]:text-foreground-light" />
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="top">{tab.description}</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</div>
<span className="text-xs text-foreground-muted group-hover:text-foreground-lighter group-data-[state=active]:text-foreground-lighter transition">
{tab.id === LINTER_LEVELS.ERROR && lintCountLabel(errorLintsCount, 'errors')}
{tab.id === LINTER_LEVELS.WARN && lintCountLabel(warnLintsCount, 'warnings')}
{tab.id === LINTER_LEVELS.INFO && lintCountLabel(infoLintsCount, 'suggestions')}
</span>
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
</Tabs_Shadcn_>
<div className="bg-surface-200 p-2 px-6 py-2 border-t -mt-px">
{LINT_TABS.map((tab) => (
<div key={tab.id} className={tab.id === currentTab ? '' : 'hidden'}>
<FilterPopover
name="Filter"
options={filterOptions}
labelKey="name"
valueKey="value"
activeOptions={filters.find((filter) => filter.level === currentTab)?.filters || []}
onSaveFilters={(values) => updateFilters(currentTab, values)}
/>
</div>
))}
</div>
<div className="col-span-12 flex items-center justify-between">
<div className="flex items-center gap-x-4"></div>
</div>
<LoadingLine loading={isRefetching} />
<ResizablePanelGroup
direction="horizontal"
className="relative flex flex-grow bg-alternative min-h-0"
autoSaveId="linter-layout-v1"
>
<ResizablePanel defaultSize={1}>
<DataGrid
ref={gridRef}
style={{ height: '100%' }}
className={cn('flex-1 flex-grow h-full')}
rowHeight={44}
headerRowHeight={36}
columns={columns}
rows={filteredLints ?? []}
rowClass={(_, idx) => {
const isSelected = idx === selectedRow
return [
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`,
`${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`,
'[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
'[&>.rdg-cell:first-child>div]:ml-4',
].join(' ')
}}
renderers={{
renderRow(idx, props) {
return (
<Row
{...props}
onClick={() => {
if (typeof idx === 'number' && idx >= 0) {
setSelectedRow(idx)
setSelectedLint(props.row)
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
}
}}
/>
)
},
noRowsFallback: isLoading ? (
<div className="absolute top-14 px-6 w-full">
<GenericSkeletonLoader />
</div>
) : (
<div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
<TextSearch className="text-foreground-muted" strokeWidth={1} />
<div className="text-center">
<p className="text-foreground">No issues detected</p>
<p className="text-foreground-light">
Congrats! There are no suggestions available for this database
</p>
</div>
</div>
),
}}
/>
</ResizablePanel>
{selectedLint !== null && (
<>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={30}
maxSize={45}
minSize={30}
className="bg-studio border-t"
>
<Button
type="text"
className="absolute top-3 right-3 px-1"
icon={<X size={14} />}
onClick={() => {
setSelectedLint(null)
setSelectedRow(undefined)
}}
/>
<Tabs_Shadcn_
value={view}
className="flex flex-col h-full"
onValueChange={(value: any) => {
setView(value)
}}
>
<TabsList_Shadcn_ className="px-5 flex gap-x-4 min-h-[46px]">
<TabsTrigger_Shadcn_
value="details"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
>
Overview
</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
<TabsContent_Shadcn_
value="details"
className="mt-0 flex-grow min-h-0 overflow-y-auto prose"
>
{selectedLint && (
<div className={cn('py-4 px-5')}>
<h3 className="text-sm">
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
</h3>
<div className="flex items-center gap-2 text-sm">
<span>Entity</span>
<div className="flex items-center gap-1 px-2 py-0.5 bg-surface-200 border rounded-lg ">
{selectedLint.metadata?.type === 'table' && (
<Table2 className="text-foreground-muted" size={15} strokeWidth={1} />
)}
{selectedLint.metadata?.type === 'view' && (
<Eye className="text-foreground-muted" size={15} strokeWidth={1.5} />
)}{' '}
{`${selectedLint.metadata?.schema}.${selectedLint.metadata?.name}`}
</div>
</div>
<div className="grid">
<div>
<h3 className="text-sm">Issue</h3>
<ReactMarkdown className="leading-6 text-sm">
{selectedLint.detail}
</ReactMarkdown>
</div>
<div>
<h3 className="text-sm">Description</h3>
<ReactMarkdown className="text-sm">
{selectedLint.description}
</ReactMarkdown>
</div>
<div className="grid gap-2">
<h3 className="text-sm">Resolve</h3>
<div>
<LintCTA
title={selectedLint.name}
projectRef={ref!}
metadata={selectedLint.metadata}
/>
</div>
) : (
<div className="flex items-center gap-x-2">
<Check size={12} />
<p className="text-foreground-light">
No issues have been found for this database
</p>
</div>
)}
</Table.td>
</Table.tr>,
]
: (filters.levels.length > 0 || filters.types.length > 0) &&
filteredLints.length === 0
? [
<Table.tr key="empty-state">
<Table.td colSpan={6} className="p-3 py-12">
<p className="text-foreground-light">
No problems found based on the selected filters
</p>
</Table.td>
</Table.tr>,
]
: filteredLints.map((lint) => {
return <ReportLintsTableRow key={lint.cache_key} lint={lint} />
})),
]}
</div>
</div>
</div>
)}
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
<div className="px-6 py-6 flex gap-x-4 border-t ">
<div className="w-[35%] flex flex-col gap-y-1 text-sm">
<p>Reset suggestions</p>
<p className="text-xs text-foreground-light">
Consider resetting the analysis making any changes
</p>
<Button
type="default"
className="!mt-3 w-min"
disabled={isLoading || isRefetching}
loading={isLoading || isRefetching}
onClick={() => refetch()}
>
Rerun linter
</Button>
</div>
<div className="w-[35%] flex flex-col gap-y-1 text-sm">
<p>How are these suggestions generated?</p>
<Markdown
className="text-xs"
content="These suggestions use [splinter (Supabase Postgres LINTER)](https://github.com/supabase/splinter)."
/>
</div>
{/* {ignoredLints.length > 0 && (
<div className="col-span-12 flex flex-col text-sm max-w-none gap-8 py-4">
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="1" className="border-none">
<AccordionTrigger className="px-4 bg-surface-100 rounded border [&[data-state=open]]:rounded-b-none hover:no-underline">
<div className="text-sm text-foreground-light font-normal">
Ignored problems ({ignoredLints.length})
</div>
</AccordionTrigger>
<AccordionContent_Shadcn_>
<Table
body={
(lints ?? []).length === 0 ? (
<Table.tr>
<Table.td colSpan={6} className="p-3 py-12 text-center">
<p className="text-foreground-light">
{isLoading ? (
<>
<Loader className="animate-spin" size={12} />
Checking for database issues
</>
) : (
'No issues have been found for this database'
)}
</p>
</Table.td>
</Table.tr>
) : (
<>
{ignoredLints.map((lint) => {
return <ReportLintsTableRow key={lint.cache_key} lint={lint} />
})}
</>
)
}
/>
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
</div>
)} */}
</ScaffoldSection>
</ScaffoldContainer>
</div>
</div>
)
}