mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
more fine tuning
This commit is contained in:
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user