more fine tuning

This commit is contained in:
Saxon Fletcher committed 2025-08-29 12:42:40 +10:00
1 parent a2675b6a82
commit 71b550ad29
9 files changed
+502 -443

No files matched your search

@@ -2,7 +2,7 @@ import type { PostgresSchema } from '@supabase/postgres-meta'
import { toPng, toSvg } from 'html-to-image'
import { Check, Clipboard, Download, Loader2 } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
import 'reactflow/dist/style.css'
import { toast } from 'sonner'
@@ -30,12 +30,7 @@ import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.u
import { TableNode } from './SchemaTableNode'
// [Joshen] Persisting logic: Only save positions to local storage WHEN a node is moved OR when explicitly clicked to reset layout
interface SchemaGraphProps {
hideUI?: boolean
onLoad?: () => void
}
export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
export const SchemaGraph = () => {
const { ref } = useParams()
const { resolvedTheme } = useTheme()
const { data: project } = useSelectedProjectQuery()
@@ -93,8 +88,6 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
{}
)
const hasFiredOnLoadRef = useRef(false)
const resetLayout = () => {
const nodes = reactFlowInstance.getNodes()
const edges = reactFlowInstance.getEdges()
@@ -186,110 +179,94 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
getGraphDataFromTables(ref as string, schema, tables).then(({ nodes, edges }) => {
reactFlowInstance.setNodes(nodes)
reactFlowInstance.setEdges(edges)
setTimeout(() => {
reactFlowInstance.fitView({}) // it needs to happen during next event tick
if (!hasFiredOnLoadRef.current) {
hasFiredOnLoadRef.current = true
onLoad?.()
}
})
setTimeout(() => reactFlowInstance.fitView({})) // it needs to happen during next event tick
})
}
}, [isSuccessTables, isSuccessSchemas, tables, resolvedTheme])
// Fire onLoad even if there are no tables (successful load but empty state)
useEffect(() => {
if (isSuccessTables && isSuccessSchemas && tables.length === 0 && !hasFiredOnLoadRef.current) {
hasFiredOnLoadRef.current = true
onLoad?.()
}
}, [isSuccessTables, isSuccessSchemas, tables])
return (
<>
{!hideUI && (
<div className="flex items-center justify-between p-4 border-b border-muted">
{isLoadingSchemas && (
<div className="h-[34px] w-[260px] bg-foreground-lighter rounded shimmering-loader" />
)}
<div className="flex items-center justify-between p-4 border-b border-muted">
{isLoadingSchemas && (
<div className="h-[34px] w-[260px] bg-foreground-lighter rounded shimmering-loader" />
)}
{isErrorSchemas && (
<AlertError error={errorSchemas as any} subject="Failed to retrieve schemas" />
)}
{isErrorSchemas && (
<AlertError error={errorSchemas as any} subject="Failed to retrieve schemas" />
)}
{isSuccessSchemas && (
<>
<SchemaSelector
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<div className="flex items-center gap-x-2">
<ButtonTooltip
type="outline"
icon={copied ? <Check /> : <Clipboard />}
onClick={() => {
if (tables) {
copyToClipboard(tablesToSQL(tables))
setCopied(true)
}
}}
tooltip={{
content: {
side: 'bottom',
text: (
<div className="max-w-[180px] space-y-2 text-foreground-light">
<p className="text-foreground">Note</p>
<p>
This schema is for context or debugging only. Table order and
constraints may be invalid. Not meant to be run as-is.
</p>
</div>
),
},
}}
>
Copy as SQL
</ButtonTooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
aria-label="Download Schema"
type="default"
loading={isDownloading}
className="px-1.5"
icon={<Download />}
tooltip={{ content: { side: 'bottom', text: 'Download current view' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32">
<DropdownMenuItem onClick={() => downloadImage('png')}>
Download as PNG
</DropdownMenuItem>
<DropdownMenuItem onClick={() => downloadImage('svg')}>
Download as SVG
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ButtonTooltip
type="default"
onClick={resetLayout}
tooltip={{
content: {
side: 'bottom',
text: 'Automatically arrange the layout of all nodes',
},
}}
>
Auto layout
</ButtonTooltip>
</div>
</>
)}
</div>
)}
{isSuccessSchemas && (
<>
<SchemaSelector
className="w-[180px]"
size="tiny"
showError={false}
selectedSchemaName={selectedSchema}
onSelectSchema={setSelectedSchema}
/>
<div className="flex items-center gap-x-2">
<ButtonTooltip
type="outline"
icon={copied ? <Check /> : <Clipboard />}
onClick={() => {
if (tables) {
copyToClipboard(tablesToSQL(tables))
setCopied(true)
}
}}
tooltip={{
content: {
side: 'bottom',
text: (
<div className="max-w-[180px] space-y-2 text-foreground-light">
<p className="text-foreground">Note</p>
<p>
This schema is for context or debugging only. Table order and constraints
may be invalid. Not meant to be run as-is.
</p>
</div>
),
},
}}
>
Copy as SQL
</ButtonTooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
aria-label="Download Schema"
type="default"
loading={isDownloading}
className="px-1.5"
icon={<Download />}
tooltip={{ content: { side: 'bottom', text: 'Download current view' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32">
<DropdownMenuItem onClick={() => downloadImage('png')}>
Download as PNG
</DropdownMenuItem>
<DropdownMenuItem onClick={() => downloadImage('svg')}>
Download as SVG
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ButtonTooltip
type="default"
onClick={resetLayout}
tooltip={{
content: {
side: 'bottom',
text: 'Automatically arrange the layout of all nodes',
},
}}
>
Auto layout
</ButtonTooltip>
</div>
</>
)}
</div>
{isLoadingTables && (
<div className="w-full h-full flex items-center justify-center gap-x-2">
<Loader2 className="animate-spin text-foreground-light" size={16} />
@@ -303,7 +280,7 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
)}
{isSuccessTables && (
<>
{tables.length === 0 && !hideUI ? (
{tables.length === 0 ? (
<div className="h-full flex items-center justify-center">
<ProductEmptyState
title="No tables created yet"
@@ -331,9 +308,6 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
}}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{
padding: hideUI ? 48 : 0,
}}
minZoom={0.8}
maxZoom={1.8}
proOptions={{ hideAttribution: true }}
@@ -341,22 +315,18 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => {
>
<Background
gap={16}
className="[&>*]:stroke-foreground-muted opacity-[50%]"
className="[&>*]:stroke-foreground-muted opacity-[25%]"
variant={BackgroundVariant.Dots}
color={'inherit'}
/>
{!hideUI && (
<>
<MiniMap
pannable
zoomable
nodeColor={miniMapNodeColor}
maskColor={miniMapMaskColor}
className="border rounded-md shadow-sm"
/>
<SchemaGraphLegend />
</>
)}
<MiniMap
pannable
zoomable
nodeColor={miniMapNodeColor}
maskColor={miniMapMaskColor}
className="border rounded-md shadow-sm"
/>
<SchemaGraphLegend />
</ReactFlow>
</div>
)}
@@ -1,16 +1,28 @@
import { BarChart, Shield } from 'lucide-react'
import Link from 'next/link'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { EntityTypeIcon, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import { LintCategoryBadge, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
import {
AiIconAnimation,
Button,
Card,
CardContent,
CardHeader,
CardTitle,
Sheet,
SheetContent,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { Row } from 'ui-patterns'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import LintDetail from 'components/interfaces/Linter/LintDetail'
// Returns the first sentence of the provided text. Uses Intl.Segmenter when available
// and falls back to a regex that avoids splitting on intra-word dots (e.g. `public.messages`).
@@ -45,6 +57,8 @@ export const AdvisorWidget = () => {
const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef })
const snap = useAiAssistantStateSnapshot()
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
const securityLints = useMemo(
() => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')),
[lints]
@@ -113,54 +127,77 @@ export const AdvisorWidget = () => {
<ShimmeringLoader className="w-1/2" />
</div>
) : combinedIssues.length > 0 ? (
<Row columns={[3, 2, 1]}>
{combinedIssues.map((lint) => {
console.log(lint)
const advisor = getAdvisorPath(lint)
return (
<Card key={lint.cache_key} className="h-full flex flex-col items-stretch h-64">
<CardHeader className="border-b-0 shrink-0 flex flex-row gap-2 space-y-0 justify-between items-center">
<Link
href={`/project/${projectRef}/advisors/${advisor}?id=${lint.cache_key}&preset=${lint.level}`}
className="flex flex-row items-center gap-3"
>
{lint.categories[0] === 'SECURITY' ? (
<Shield size={16} strokeWidth={1.5} className="text-foreground-muted" />
) : (
<BarChart size={16} strokeWidth={1.5} className="text-foreground-muted" />
)}
<CardTitle className="text-foreground-light">{lint.categories[0]}</CardTitle>
</Link>
<ButtonTooltip
type="text"
className="w-7 h-7"
icon={<AiIconAnimation size={16} />}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: `Summarize the issue and suggest fixes for the following lint item:
<>
<Row columns={[3, 2, 1]}>
{combinedIssues.map((lint) => {
const advisor = getAdvisorPath(lint)
return (
<Card
key={lint.cache_key}
className="h-full flex flex-col items-stretch h-64 cursor-pointer"
onClick={() => {
setSelectedLint(lint)
}}
>
<CardHeader className="border-b-0 shrink-0 flex flex-row gap-2 space-y-0 justify-between items-center">
<div className="flex flex-row items-center gap-3">
{lint.categories[0] === 'SECURITY' ? (
<Shield size={16} strokeWidth={1.5} className="text-foreground-muted" />
) : (
<BarChart size={16} strokeWidth={1.5} className="text-foreground-muted" />
)}
<CardTitle className="text-foreground-light">{lint.categories[0]}</CardTitle>
</div>
<ButtonTooltip
type="text"
className="w-7 h-7"
icon={<AiIconAnimation size={16} />}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: `Summarize the issue and suggest fixes for the following lint item:
Title: ${lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title}
Entity: ${(lint.metadata && (lint.metadata.entity || (lint.metadata.schema && lint.metadata.name && `${lint.metadata.schema}.${lint.metadata.name}`))) ?? 'N/A'}
Schema: ${lint.metadata?.schema ?? 'N/A'}
Issue Details: ${lint.detail ? lint.detail.replace(/\`/g, '`') : 'N/A'}
Description: ${lint.description ? lint.description.replace(/\`/g, '`') : 'N/A'}`,
})
}}
tooltip={{
content: { side: 'bottom', text: 'Help me fix this issue' },
}}
/>
</CardHeader>
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
{lint.detail ? getFirstSentence(lint.detail) : lint.title}
</CardContent>
</Card>
)
})}
</Row>
})
}}
tooltip={{
content: { side: 'bottom', text: 'Help me fix this issue' },
}}
/>
</CardHeader>
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
{lint.detail ? getFirstSentence(lint.detail) : lint.title}
</CardContent>
</Card>
)
})}
</Row>
{selectedLint && (
<Sheet open={selectedLint !== null} onOpenChange={() => setSelectedLint(null)}>
<SheetContent>
<SheetHeader>
<div className="flex items-center gap-4">
<SheetTitle>
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
</SheetTitle>
<LintCategoryBadge category={selectedLint.categories[0]} />
</div>
</SheetHeader>
<SheetSection>
{selectedLint && projectRef && (
<LintDetail lint={selectedLint} projectRef={projectRef!} />
)}
</SheetSection>
</SheetContent>
</Sheet>
)}
</>
) : (
<Card className="bg-transparent">
<CardContent className="flex flex-col items-center justify-center gap-2 p-16">
@@ -27,6 +27,7 @@ export interface GettingStartedProps {
className?: string
headerRight?: React.ReactNode
emptyContent?: React.ReactNode
onDismiss?: () => void
}
export function GettingStarted({
@@ -34,6 +35,7 @@ export function GettingStarted({
className,
headerRight,
emptyContent,
onDismiss,
}: GettingStartedProps) {
return (
<section className={cn('w-full', className)}>
@@ -41,7 +43,7 @@ export function GettingStarted({
<h3 className="heading-section">Getting started</h3>
<div className="flex items-center gap-2">
{headerRight}
<Button size="tiny" type="outline">
<Button size="tiny" type="outline" onClick={onDismiss}>
Dismiss
</Button>
</div>
@@ -33,7 +33,15 @@ import {
} from 'ui'
import { BASE_PATH } from 'lib/constants'
export default function GettingStartedSection() {
type GettingStartedState = 'empty' | 'code' | 'no-code' | 'hidden'
export default function GettingStartedSection({
value,
onChange,
}: {
value: GettingStartedState
onChange: (v: GettingStartedState) => void
}) {
const { data: project } = useSelectedProjectQuery()
const { ref } = useParams()
const aiSnap = useAiAssistantStateSnapshot()
@@ -41,7 +49,7 @@ export default function GettingStartedSection() {
// Local state for framework selector preview
const [selectedFramework, setSelectedFramework] = useState<string>(FRAMEWORKS[0]?.key ?? 'nextjs')
const [workflow, setWorkflow] = useState<'no-code' | 'code' | null>(null)
const workflow: 'no-code' | 'code' | null = value === 'code' || value === 'no-code' ? value : null
const { data: tablesData } = useTablesQuery({
projectRef: project?.ref,
@@ -68,6 +76,7 @@ export default function GettingStartedSection() {
description: 'Install the Supabase CLI for local development, migrations, and seeding.',
actions: [
{
label: 'Install via npm',
component: (
<CodeBlock className="w-full text-xs p-3 !bg" language="bash">
npm install supabase --save-dev
@@ -466,11 +475,12 @@ export default function GettingStartedSection() {
return (
<GettingStarted
onDismiss={() => onChange('hidden')}
headerRight={
<ToggleGroup
type="single"
value={workflow ?? undefined}
onValueChange={(v) => setWorkflow((v as 'no-code' | 'code') ?? null)}
onValueChange={(v) => v && onChange(v as 'no-code' | 'code')}
>
<ToggleGroupItem
value="no-code"
@@ -504,23 +514,23 @@ export default function GettingStartedSection() {
/>
<div className="absolute inset-0 bg-gradient-to-r from-background-alternative to-transparent" />
</div>
<CardContent className="relative z-10 p-12 grid grid-cols-2 gap-16 items-center justify-center">
<CardContent className="relative z-10 p-8 md:p-12 grid lg:grid-cols-2 gap-16 items-center">
<div>
<h2 className="heading-subSection mb-0 heading-meta text-foreground-light mb-4">
Choose a preferred workflow
</h2>
<p className="text-foreground mb-6">
<p className="text-foreground">
With Supabase, you have the flexibility to adopt a workflow that works for you. You
can do everything via the dashboard, or manage your entire project within your own
codebase.
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-stretch gap-4">
<Button
size="medium"
type="outline"
onClick={() => setWorkflow('no-code')}
className="block gap-2 h-auto p-8 w-80 text-left justify-start bg-background "
onClick={() => onChange('no-code')}
className="block gap-2 h-auto p-4 md:p-8 max-w-80 text-left justify-start bg-background "
>
<Table2 size={20} strokeWidth={1.5} className="text-brand" />
<div className="mt-4">
@@ -533,8 +543,8 @@ export default function GettingStartedSection() {
<Button
size="medium"
type="outline"
onClick={() => setWorkflow('code')}
className="bg-background block gap-2 h-auto p-8 w-80 text-left justify-start"
onClick={() => onChange('code')}
className="bg-background block gap-2 h-auto p-4 md:p-8 max-w-80 text-left justify-start"
>
<Code size={20} strokeWidth={1.5} className="text-brand" />
<div className="mt-4">
@@ -90,6 +90,16 @@ const ProjectUsage = () => {
}
}, [selectedInterval]) // Only recalculate when interval changes
// Compute the previous period range for deltas
const { previousStart, previousEnd } = useMemo(() => {
const currentStart = dayjs(timestampStart)
const currentEnd = dayjs(timestampEnd)
const durationMs = currentEnd.diff(currentStart)
const prevEnd = currentStart
const prevStart = currentStart.subtract(durationMs, 'millisecond')
return { previousStart: prevStart.toISOString(), previousEnd: prevEnd.toISOString() }
}, [timestampStart, timestampEnd])
// Fetch per-service event chart data
const dbStats = useProjectUsageStats({
projectRef: projectRef as string,
@@ -126,6 +136,42 @@ const ProjectUsage = () => {
timestampEnd,
})
// Fetch previous period data for deltas
const dbStatsPrev = useProjectUsageStats({
projectRef: projectRef as string,
table: LogsTableName.EDGE,
timestampStart: previousStart,
timestampEnd: previousEnd,
})
const fnEdgeStatsPrev = useProjectUsageStats({
projectRef: projectRef as string,
table: LogsTableName.FN_EDGE,
timestampStart: previousStart,
timestampEnd: previousEnd,
})
const authStatsPrev = useProjectUsageStats({
projectRef: projectRef as string,
table: LogsTableName.AUTH,
timestampStart: previousStart,
timestampEnd: previousEnd,
})
const storageStatsPrev = useProjectUsageStats({
projectRef: projectRef as string,
table: LogsTableName.EDGE,
timestampStart: previousStart,
timestampEnd: previousEnd,
})
const realtimeStatsPrev = useProjectUsageStats({
projectRef: projectRef as string,
table: LogsTableName.REALTIME,
timestampStart: previousStart,
timestampEnd: previousEnd,
})
// Transform eventChartData into LogsBarChart format
type LogsBarChartDatum = {
timestamp: string
@@ -243,8 +289,39 @@ const ProjectUsage = () => {
const totalErrors = enabledServices.reduce((sum, s) => sum + (s.err || 0), 0)
const errorRate = totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0
const totalRequestsChangePct = 0
const errorRateChangePct = 0
// Compute previous totals for enabled services
const prevServiceTotals = [
{ enabled: true, stats: dbStatsPrev },
{ enabled: true, stats: fnEdgeStatsPrev },
{ enabled: authEnabled, stats: authStatsPrev },
{ enabled: storageEnabled, stats: storageStatsPrev },
{ enabled: true, stats: realtimeStatsPrev },
].map((entry) => {
const data = toLogsBarChartData(entry.stats.eventChartData)
return {
enabled: entry.enabled,
total: sumTotal(data || []),
err: sumErrors(data || []),
}
})
const enabledPrev = prevServiceTotals.filter((s) => s.enabled)
const prevTotalRequests = enabledPrev.reduce((sum, s) => sum + (s.total || 0), 0)
const prevTotalErrors = enabledPrev.reduce((sum, s) => sum + (s.err || 0), 0)
const prevErrorRate = prevTotalRequests > 0 ? (prevTotalErrors / prevTotalRequests) * 100 : 0
const totalRequestsChangePct =
prevTotalRequests === 0
? totalRequests > 0
? 100
: 0
: ((totalRequests - prevTotalRequests) / prevTotalRequests) * 100
const errorRateChangePct =
prevErrorRate === 0
? errorRate > 0
? 100
: 0
: ((errorRate - prevErrorRate) / prevErrorRate) * 100
const formatDelta = (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`
const totalDeltaClass = totalRequestsChangePct >= 0 ? 'text-brand' : 'text-destructive'
const errorDeltaClass = errorRateChangePct <= 0 ? 'text-brand' : 'text-destructive'
@@ -325,14 +402,34 @@ const ProjectUsage = () => {
<Row columns={[3, 2, 1]}>
{enabledServices.map((s) => (
<Card key={s.key} className="mb-0 md:mb-0 h-full flex flex-col">
<PanelHeader
icon={s.icon}
title={s.title}
href={s.href}
total={s.total}
warn={s.warn}
err={s.err}
/>
<CardHeader className="flex flex-row items-end justify-between gap-2 space-y-0 pb-0 border-b-0">
<div className="flex items-center gap-2">
<div className="flex flex-col">
<div className="flex items-center gap-2">
<CardTitle className="text-foreground-light">
{s.href ? <Link href={s.href}>{s.title}</Link> : s.title}
</CardTitle>
</div>
<span className="text-foreground text-xl">{(s.total || 0).toLocaleString()}</span>
</div>
</div>
<div className="flex items-end gap-4 text-foreground-light">
<div className="flex flex-col items-end">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-warning rounded-full" />
<span className="heading-meta">Warn</span>
</div>
<span className="text-foreground text-xl">{(s.warn || 0).toLocaleString()}</span>
</div>
<div className="flex flex-col items-end">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-destructive rounded-full" />
<span className="heading-meta">Err</span>
</div>
<span className="text-foreground text-xl">{(s.err || 0).toLocaleString()}</span>
</div>
</div>
</CardHeader>
<CardContent className="p-6 pt-4 flex-1">
<Loading active={isLoading}>
<LogsBarChart
@@ -353,48 +450,3 @@ const ProjectUsage = () => {
)
}
export default ProjectUsage
const PanelHeader = ({
icon,
title,
href,
total,
warn,
err,
}: {
icon?: React.ReactNode
title: string
href?: string
total?: number
warn?: number
err?: number
}) => (
<CardHeader className="flex flex-row items-end justify-between gap-2 space-y-0 pb-0 border-b-0">
<div className="flex items-center gap-2">
<div className="flex flex-col">
<div className="flex items-center gap-2">
<CardTitle className="text-foreground-light">
{href ? <Link href={href}>{title}</Link> : title}
</CardTitle>
</div>
<span className="text-foreground text-xl">{(total || 0).toLocaleString()}</span>
</div>
</div>
<div className="flex items-end gap-4 text-foreground-light">
<div className="flex flex-col items-end">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-warning rounded-full" />
<span className="heading-meta">Warn</span>
</div>
<span className="text-foreground text-xl">{(warn || 0).toLocaleString()}</span>
</div>
<div className="flex flex-col items-end">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-destructive rounded-full" />
<span className="heading-meta">Err</span>
</div>
<span className="text-foreground text-xl">{(err || 0).toLocaleString()}</span>
</div>
</div>
</CardHeader>
)
@@ -9,6 +9,7 @@ import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
import { InlineLink } from 'components/ui/InlineLink'
import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner'
import { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import InstanceConfiguration from '../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration'
interface TopSectionProps {
projectName: string
@@ -30,78 +31,66 @@ const TopSection = ({
organization,
isPaused,
}: TopSectionProps) => {
const [graphLoaded, setGraphLoaded] = useState(false)
return (
<div className="px-8">
<div className="mx-auto max-w-7xl flex flex-col gap-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 py-16 md:p-0 w-full items-center">
<div className="flex-1 flex flex-col">
<div className="flex flex-col md:flex-row md:items-end gap-3 w-full">
<div>
{!isMainBranch && (
<Link
href={`/project/${parentProject?.ref}`}
className="text-sm text-foreground-light"
>
{parentProject?.name}
</Link>
)}
<h1 className="text-3xl">{projectName}</h1>
</div>
<div className="flex items-center gap-x-2 mb-1">
{isOrioleDb && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="warning">OrioleDB</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
This project is using Postgres with OrioleDB which is currently in preview and
not suitable for production workloads. View our{' '}
<InlineLink href="https://supabase.com/docs/guides/database/orioledb">
documentation
</InlineLink>{' '}
for all limitations.
</TooltipContent>
</Tooltip>
)}
<ComputeBadgeWrapper
project={{
ref: project?.ref,
organization_slug: organization?.slug,
cloud_provider: project?.cloud_provider,
infra_compute_size: project?.infra_compute_size,
}}
/>
</div>
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 py-16 md:p-0 w-full items-center">
<div className="flex flex-col">
<div className="flex flex-col md:flex-row md:items-end gap-3 w-full">
<div>
{!isMainBranch && (
<Link
href={`/project/${parentProject?.ref}`}
className="text-sm text-foreground-light"
>
{parentProject?.name}
</Link>
)}
<h1 className="text-3xl">{projectName}</h1>
</div>
<div className="mt-8">
<ActivityStats />
<div className="flex items-center gap-x-2 mb-1">
{isOrioleDb && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="warning">OrioleDB</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
This project is using Postgres with OrioleDB which is currently in preview and
not suitable for production workloads. View our{' '}
<InlineLink href="https://supabase.com/docs/guides/database/orioledb">
documentation
</InlineLink>{' '}
for all limitations.
</TooltipContent>
</Tooltip>
)}
<ComputeBadgeWrapper
project={{
ref: project?.ref,
organization_slug: organization?.slug,
cloud_provider: project?.cloud_provider,
infra_compute_size: project?.infra_compute_size,
}}
/>
</div>
</div>
<div>
<div
className={cn(
'w-full h-[400px] md:h-[600px] overflow-hidden flex flex-col relative',
graphLoaded ? 'cursor-default' : 'cursor-move opacity-0'
)}
>
<ReactFlowProvider>
<SchemaGraph hideUI onLoad={() => setGraphLoaded(true)} />
</ReactFlowProvider>
<div
className={`absolute inset-y-0 left-0 bg-gradient-to-r from-background-surface-75 to-transparent pointer-events-none transition-all duration-700 ease-out ${
graphLoaded ? 'w-32' : 'w-[200%]'
}`}
/>
<div className="absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-background-surface-75 to-transparent pointer-events-none" />
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-background-surface-75 to-transparent pointer-events-none" />
</div>
<div className="mt-8">
<ActivityStats />
</div>
</div>
<div>
<div
className={cn(
'w-full h-[400px] md:h-[500px] border border-muted rounded-md overflow-hidden flex flex-col relative'
)}
>
<ReactFlowProvider>
<InstanceConfiguration diagramOnly />
</ReactFlowProvider>
</div>
</div>
<ProjectUpgradeFailedBanner />
{isPaused && <ProjectPausedState />}
</div>
<ProjectUpgradeFailedBanner />
{isPaused && <ProjectPausedState />}
</div>
)
}
@@ -0,0 +1,84 @@
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import { EntityTypeIcon, LintCTA, LintCategoryBadge, LintEntity } from './Linter.utils'
import { Lint } from 'data/lint/lint-query'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Button } from 'ui'
import { ExternalLink } from 'lucide-react'
interface LintDetailProps {
lint: Lint
projectRef: string
}
const LintDetail = ({ lint, projectRef }: LintDetailProps) => {
const snap = useAiAssistantStateSnapshot()
return (
<div>
<h3 className="text-sm mb-2">Entity</h3>
<div className="flex items-center gap-1 px-2 py-0.5 bg-surface-200 border rounded-lg text-sm mb-6">
<EntityTypeIcon type={lint.metadata?.type} />
<LintEntity metadata={lint.metadata} />
</div>
<h3 className="text-sm mb-2">Issue</h3>
<ReactMarkdown className="leading-6 text-sm text-foreground-light mb-6">
{lint.detail.replace(/\`/g, '`')}
</ReactMarkdown>
<h3 className="text-sm mb-2">Description</h3>
<ReactMarkdown className="text-sm text-foreground-light mb-6">
{lint.description.replace(/\`/g, '`')}
</ReactMarkdown>
<h3 className="text-sm mb-2">Resolve</h3>
<div className="flex items-center gap-2">
<Button
icon={<AiIconAnimation className="scale-75 w-3 h-3" />}
onClick={() => {
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: `Summarize the issue and suggest fixes: ${
lintInfoMap.find((item) => item.name === lint.name)?.title
}
\nEntity: ${
(lint.metadata &&
(lint.metadata.entity ||
(lint.metadata.schema &&
lint.metadata.name &&
`${lint.metadata.schema}.${lint.metadata.name}`))) ??
''
}
\nSchema: ${lint.metadata?.schema ?? ''}
\nIssue: ${lint.detail.replace(/\\`/g, '`')}
\nDescription: ${lint.description.replace(/\\`/g, '`')}\n`,
})
}}
>
Ask Assistant
</Button>
<LintCTA title={lint.name} projectRef={projectRef} metadata={lint.metadata} />
<Button asChild type="text">
<Link
href={
lintInfoMap.find((item) => item.name === lint.name)?.docsLink ||
'https://supabase.com/docs/guides/database/database-linter'
}
target="_blank"
rel="noreferrer"
className="no-underline"
>
<span className="flex items-center gap-2">
Learn more <ExternalLink size={14} />
</span>
</Link>
</Button>
</div>
</div>
)
}
export default LintDetail
@@ -1,17 +1,19 @@
import { ExternalLink, X } from 'lucide-react'
import Link from 'next/link'
import { X } from 'lucide-react'
import { useRef, useState } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import ReactMarkdown from 'react-markdown'
import { useParams } from 'common'
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
import { LintEntity, NoIssuesFound, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
import {
LintCategoryBadge,
LintEntity,
NoIssuesFound,
lintInfoMap,
} from 'components/interfaces/Linter/Linter.utils'
import { Lint } from 'data/lint/lint-query'
import { useRouter } from 'next/router'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import {
AiIconAnimation,
Button,
ResizableHandle,
ResizablePanel,
@@ -23,7 +25,8 @@ import {
cn,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { EntityTypeIcon, LintCTA, LintCategoryBadge } from './Linter.utils'
import { EntityTypeIcon } from './Linter.utils'
import LintDetail from './LintDetail'
interface LinterDataGridProps {
isLoading: boolean
@@ -43,7 +46,6 @@ const LinterDataGrid = ({
const gridRef = useRef<DataGridHandle>(null)
const { ref } = useParams()
const router = useRouter()
const snap = useAiAssistantStateSnapshot()
const [view, setView] = useState<'details' | 'suggestion'>('details')
@@ -181,109 +183,18 @@ const LinterDataGrid = ({
<>
<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 />}
onClick={handleSidepanelClose}
/>
<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="py-4 px-5">
<div className="flex items-center gap-2 py-2">
<h3 className="text-sm m-0">
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
</h3>
<LintCategoryBadge category={selectedLint.categories[0]} />
</div>
<div className="flex items-center gap-2 text-sm mt-4">
<span>Entity</span>
<div className="flex items-center gap-1 px-2 py-0.5 bg-surface-200 border rounded-lg ">
<EntityTypeIcon type={selectedLint.metadata?.type} />
<LintEntity metadata={selectedLint.metadata} />
</div>
</div>
<div className="grid">
<div>
<h3 className="text-sm">Issue</h3>
<ReactMarkdown className="leading-6 text-sm">
{selectedLint.detail.replace(/\\`/g, '`')}
</ReactMarkdown>
</div>
<div>
<h3 className="text-sm">Description</h3>
<ReactMarkdown className="text-sm">
{selectedLint.description.replace(/\\`/g, '`')}
</ReactMarkdown>
</div>
<div className="grid gap-2">
<h3 className="text-sm">Resolve</h3>
<div className="flex items-center gap-2">
<Button
icon={<AiIconAnimation className="scale-75 w-3 h-3" />}
onClick={() => {
snap.newChat({
name: 'Summarize lint',
open: true,
initialInput: `Summarize the issue and suggest fixes: ${lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
\nEntity: ${(selectedLint.metadata && (selectedLint.metadata.entity || (selectedLint.metadata.schema && selectedLint.metadata.name && `${selectedLint.metadata.schema}.${selectedLint.metadata.name}`))) ?? ''}
\nSchema: ${selectedLint.metadata?.schema ?? ''}
\nIssue: ${selectedLint.detail.replace(/\\`/g, '`')}
\nDescription: ${selectedLint.description.replace(/\\`/g, '`')}\n`,
})
}}
>
Ask Assistant
</Button>
<LintCTA
title={selectedLint.name}
projectRef={ref!}
metadata={selectedLint.metadata}
/>
<Button asChild type="text">
<Link
href={
lintInfoMap.find((item) => item.name === selectedLint.name)
?.docsLink ||
'https://supabase.com/docs/guides/database/database-linter'
}
target="_blank"
rel="noreferrer"
className="no-underline"
>
<span className="flex items-center gap-2">
Learn more <ExternalLink size={14} />
</span>
</Link>
</Button>
</div>
</div>
</div>
</div>
)}
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
<div className="flex items-center justify-between w-full border-b py-3 px-6">
<div className="flex items-center gap-2">
<h3 className="text-sm m-0">
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title}
</h3>
<LintCategoryBadge category={selectedLint.categories[0]} />
</div>
<Button type="text" icon={<X />} onClick={handleSidepanelClose} />
</div>
<div className="p-6">
<LintDetail lint={selectedLint} projectRef={ref!} />
</div>
</ResizablePanel>
</>
)}
+37 -33
View File
@@ -1,5 +1,5 @@
import dayjs from 'dayjs'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef } from 'react'
import { useParams } from 'common'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -12,14 +12,7 @@ import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import {
DndContext,
DragEndEvent,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'
import DefaultLayout from 'components/layouts/DefaultLayout'
@@ -30,6 +23,7 @@ import AdvisorSection from 'components/interfaces/HomeNew/AdvisorSection'
import CustomReportSection from 'components/interfaces/HomeNew/CustomReportSection'
import SortableSection from 'components/interfaces/HomeNew/SortableSection'
import { ProjectUsageSection } from 'components/interfaces/HomeNew/ProjectUsageSection'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
const Home: NextPageWithLayout = () => {
const { data: project } = useSelectedProjectQuery()
@@ -70,12 +64,11 @@ const Home: NextPageWithLayout = () => {
['getting-started', 'usage', 'advisor', 'custom-report']
)
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
const [activeId, setActiveId] = useState<string | null>(null)
const [gettingStartedState, setGettingStartedState] = useLocalStorage<
'empty' | 'code' | 'no-code' | 'hidden'
>(`home-getting-started-${project?.ref || 'default'}`, 'empty')
const handleDragStart = (event: DragStartEvent) => {
setActiveId(String(event.active.id))
}
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
@@ -86,32 +79,43 @@ const Home: NextPageWithLayout = () => {
if (oldIndex === -1 || newIndex === -1) return items
return arrayMove(items, oldIndex, newIndex)
})
setActiveId(null)
}
return (
<div className="w-full">
<TopSection
projectName={projectName}
isMainBranch={isMainBranch}
parentProject={parentProject}
isOrioleDb={!!isOrioleDb}
project={project}
organization={organization}
projectRef={ref}
isPaused={isPaused}
/>
<ScaffoldContainer size="large">
<ScaffoldSection isFullWidth className="pt-16 pb-0">
<TopSection
projectName={projectName}
isMainBranch={isMainBranch}
parentProject={parentProject}
isOrioleDb={!!isOrioleDb}
project={project}
organization={organization}
projectRef={ref}
isPaused={isPaused}
/>
</ScaffoldSection>
</ScaffoldContainer>
{!isPaused && (
<div className="px-8 pb-32">
<div className="mx-auto max-w-7xl space-y-16">
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<SortableContext items={sectionOrder} strategy={verticalListSortingStrategy}>
<ScaffoldContainer size="large">
<ScaffoldSection isFullWidth className="gap-16 pb-32">
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext
items={sectionOrder.filter(
(id) => id !== 'getting-started' || gettingStartedState !== 'hidden'
)}
strategy={verticalListSortingStrategy}
>
{sectionOrder.map((id) => {
if (id === 'getting-started') {
return (
return gettingStartedState === 'hidden' ? null : (
<SortableSection key={id} id={id}>
<GettingStartedSection />
<GettingStartedSection
value={gettingStartedState}
onChange={setGettingStartedState}
/>
</SortableSection>
)
}
@@ -140,8 +144,8 @@ const Home: NextPageWithLayout = () => {
})}
</SortableContext>
</DndContext>
</div>
</div>
</ScaffoldSection>
</ScaffoldContainer>
)}
</div>
)