diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index bb793a16ac1..61e1d483f12 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -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 && ( -
- {isLoadingSchemas && ( -
- )} +
+ {isLoadingSchemas && ( +
+ )} - {isErrorSchemas && ( - - )} + {isErrorSchemas && ( + + )} - {isSuccessSchemas && ( - <> - -
- : } - onClick={() => { - if (tables) { - copyToClipboard(tablesToSQL(tables)) - setCopied(true) - } - }} - tooltip={{ - content: { - side: 'bottom', - text: ( -
-

Note

-

- This schema is for context or debugging only. Table order and - constraints may be invalid. Not meant to be run as-is. -

-
- ), - }, - }} - > - Copy as SQL -
- - - } - tooltip={{ content: { side: 'bottom', text: 'Download current view' } }} - /> - - - downloadImage('png')}> - Download as PNG - - downloadImage('svg')}> - Download as SVG - - - - - Auto layout - -
- - )} -
- )} + {isSuccessSchemas && ( + <> + +
+ : } + onClick={() => { + if (tables) { + copyToClipboard(tablesToSQL(tables)) + setCopied(true) + } + }} + tooltip={{ + content: { + side: 'bottom', + text: ( +
+

Note

+

+ This schema is for context or debugging only. Table order and constraints + may be invalid. Not meant to be run as-is. +

+
+ ), + }, + }} + > + Copy as SQL +
+ + + } + tooltip={{ content: { side: 'bottom', text: 'Download current view' } }} + /> + + + downloadImage('png')}> + Download as PNG + + downloadImage('svg')}> + Download as SVG + + + + + Auto layout + +
+ + )} +
{isLoadingTables && (
@@ -303,7 +280,7 @@ export const SchemaGraph = ({ hideUI = false, onLoad }: SchemaGraphProps) => { )} {isSuccessTables && ( <> - {tables.length === 0 && !hideUI ? ( + {tables.length === 0 ? (
{ }} 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) => { > - {!hideUI && ( - <> - - - - )} + +
)} diff --git a/apps/studio/components/interfaces/HomeNew/AdvisorWidget.tsx b/apps/studio/components/interfaces/HomeNew/AdvisorWidget.tsx index f11c359b257..3aab8001a82 100644 --- a/apps/studio/components/interfaces/HomeNew/AdvisorWidget.tsx +++ b/apps/studio/components/interfaces/HomeNew/AdvisorWidget.tsx @@ -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(null) + const securityLints = useMemo( () => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')), [lints] @@ -113,54 +127,77 @@ export const AdvisorWidget = () => {
) : combinedIssues.length > 0 ? ( - - {combinedIssues.map((lint) => { - console.log(lint) - const advisor = getAdvisorPath(lint) - return ( - - - - {lint.categories[0] === 'SECURITY' ? ( - - ) : ( - - )} - {lint.categories[0]} - - } - 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: + <> + + {combinedIssues.map((lint) => { + const advisor = getAdvisorPath(lint) + return ( + { + setSelectedLint(lint) + }} + > + +
+ {lint.categories[0] === 'SECURITY' ? ( + + ) : ( + + )} + {lint.categories[0]} +
+ } + 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' }, - }} - /> -
- - {lint.detail ? getFirstSentence(lint.detail) : lint.title} - -
- ) - })} -
+ }) + }} + tooltip={{ + content: { side: 'bottom', text: 'Help me fix this issue' }, + }} + /> +
+ + {lint.detail ? getFirstSentence(lint.detail) : lint.title} + +
+ ) + })} +
+ {selectedLint && ( + setSelectedLint(null)}> + + +
+ + {lintInfoMap.find((item) => item.name === selectedLint.name)?.title} + + +
+
+ + {selectedLint && projectRef && ( + + )} + +
+
+ )} + ) : ( diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted.tsx index 94dc18d425a..1b99b5e8979 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStarted.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted.tsx @@ -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 (
@@ -41,7 +43,7 @@ export function GettingStarted({

Getting started

{headerRight} -
diff --git a/apps/studio/components/interfaces/HomeNew/GettingStartedSection.tsx b/apps/studio/components/interfaces/HomeNew/GettingStartedSection.tsx index 4183d49eff1..3354d45756f 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStartedSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStartedSection.tsx @@ -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(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: ( npm install supabase --save-dev @@ -466,11 +475,12 @@ export default function GettingStartedSection() { return ( onChange('hidden')} headerRight={ setWorkflow((v as 'no-code' | 'code') ?? null)} + onValueChange={(v) => v && onChange(v as 'no-code' | 'code')} >
- +

Choose a preferred workflow

-

+

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.

-
+
+ + +
+
+ ) +} + +export default LintDetail diff --git a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx index a8c8873ecb3..00e7d43d5ce 100644 --- a/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx +++ b/apps/studio/components/interfaces/Linter/LinterDataGrid.tsx @@ -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(null) const { ref } = useParams() const router = useRouter() - const snap = useAiAssistantStateSnapshot() const [view, setView] = useState<'details' | 'suggestion'>('details') @@ -181,109 +183,18 @@ const LinterDataGrid = ({ <> - - - -
-
- - - )} - - +
+
+

+ {lintInfoMap.find((item) => item.name === selectedLint.name)?.title} +

+ +
+
+
+ +
)} diff --git a/apps/studio/pages/project/[ref]/home.tsx b/apps/studio/pages/project/[ref]/home.tsx index 8d5f9430b88..55f6797e22b 100644 --- a/apps/studio/pages/project/[ref]/home.tsx +++ b/apps/studio/pages/project/[ref]/home.tsx @@ -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(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 (
- + + + + + {!isPaused && ( -
-
- - + + + + id !== 'getting-started' || gettingStartedState !== 'hidden' + )} + strategy={verticalListSortingStrategy} + > {sectionOrder.map((id) => { if (id === 'getting-started') { - return ( + return gettingStartedState === 'hidden' ? null : ( - + ) } @@ -140,8 +144,8 @@ const Home: NextPageWithLayout = () => { })} -
-
+ + )}
)