mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +08:00
App-level fixes that reproduce on BOTH the Next and TanStack builds — split out of #47657 (which stays TanStack-only) for reviewability. All were found by a full-site click-through of the dashboard. ## Invalid HTML nesting (React 19 "will cause a hydration error" console errors) - **FormLayout description rendered in a `<p>`** (`packages/ui-patterns`): consumers pass arbitrary JSX (the RowEditor's `created_at` timezone note passes a `<div>` with `<p>`s) → `<p>`-in-`<p>` / `<div>`-in-`<p>`. Container is now a `<div>` with identical classes (Tailwind preflight makes them render the same). - **Switch toggles nested inside Tooltip trigger buttons** (button-in-button) in ColumnEditor ("Allow Nullable" + "Is Unique"), ExtensionRow, and PublicationsTableItem → repo-standard `TooltipTrigger asChild` + `<div>` wrapper. - **Saved log queries rendered a `<div>` directly inside `<tbody>`** (`/logs/explorer/saved`) → rows are now proper `<tr><td colSpan>` wrappers; the component itself is untouched (it's valid in its sidebar usage). - **Nested anchors in observability metric cards**: a card-level `<Link>` wrapped MetricCard's "More information" `<Link>` (identical URLs) → the chevron affordance renders as a `<span>` when no `href` is passed; clicks bubble to the card link, tooltips preserved. Design-system standalone usage unaffected. - **`objectFit="cover"` passed to modern `next/image`** on the featured integration card (unknown-prop warning) — the className already had `object-cover`; prop dropped. ## Ghost dead-snippet after deletion Deleting the active SQL snippet left its id in `useDashboardHistory` (`history.sql`), so the "SQL Editor" nav item navigated to `/sql/<deleted-id>` — content fetch 404s, no editor pane renders, and a phantom tab reappears. Fixed both ends: delete flows now purge dashboard history (and the tabs store clears a stale `previewTabId`), and `/sql/[id]` treats a snippet 404 as "clean up + `router.replace` to `/sql/new` + toast" instead of rendering the dead state. Unit tests for the store/history cleanup. ## `pg-meta` migrations query 400s on every project load `ActivityStats` on project home runs the migrations list query, whose SQL was a bare `select * from supabase_migrations.schema_migrations` — that table only exists once a migration has run, so every other project logged a failed `?key=migrations` request on every load (visible in production consoles too). The SQL is now guarded with `to_regclass` + `query_to_xml` (same pattern as the advisor lints' `storage.buckets` guard), returning zero rows instead of erroring; legacy version-only tables still work. Tested against real dockerized Postgres (absent table, populated ordering, special chars, legacy schema) + MSW hook tests. Found and verified via /test-supabase-local (browser click-through + console audit on both builds). ## To test Console must stay free of React DOM-nesting errors ("cannot be a descendant of" / "cannot contain a nested") on each surface: 1. Table editor → Insert row panel (`created_at` field renders its timezone note) and Edit column panel ("Allow Nullable"/"Is Unique" tooltips still hover). 2. `/database/extensions` and `/database/publications` → toggle switches render, tooltips hover. 3. `/logs/explorer/saved` (with ≥1 saved query) → rows render full-width inside the table, hover shows Actions. 4. `/observability` → no nested-anchor error on load; card body click and the chevron both navigate; label help-icons still show tooltips. 5. `/integrations` → no `objectFit` unknown-prop warning; featured card images still cover. 6. **Ghost snippet**: open a SQL snippet → delete it via the sidebar → click the "SQL Editor" nav item → lands on `/sql/new` (no phantom tab, no 404 content fetch). Direct-load `/sql/<random-uuid>` → toast + redirect to `/sql/new`. 7. **Migrations 400**: load project home with a project that has never run a migration → the `pg-meta/<ref>/query?key=migrations` request returns **200** with `[]` (previously a 400 on every load). Database → Migrations still lists real migrations when they exist. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **Bug Fixes** * Deleted SQL snippets are fully removed from dashboard history and stale editor/tab state; users are redirected with a toast. * Closing preview tabs no longer leaves stale references. * Improved toggle/tooltip/dialog interactions to avoid broken UI, including metric headers showing tooltips even without direct links. * Migrations display safely when migration tables/relations are missing. * **UI Improvements** * Refreshed layout for saved queries, form descriptions, and integration imagery. * **Tests** * Added coverage for snippet history cleanup, tab removal, migrations SQL behavior, and query edge cases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- ### Review feedback: `query_to_xml` breaks on Multigres (Ivan) The defensive migrations query (added here to stop the `?key=migrations` 400 when the table doesn't exist yet) originally guarded with `query_to_xml`, which is forbidden through Multigres's pooler (MUL-736 / PSQL-1318). Rewritten without `query_to_xml`/`xmltable` using the splinter#170 pattern: a PL/pgSQL `do` block guarded by `to_regclass` (PL/pgSQL defers planning, so a missing table never errors) stashes the rows into a transaction-local GUC via `set_config`, and a trailing `select` reads them back with `jsonb_array_elements`. Verified that postgres-meta sends the whole SQL as one simple-query string → single implicit transaction → the local GUC survives to the `select` and doesn't leak into the pooled connection. 6/6 dockerized-Postgres tests (absent table → `[]`, populated/ordered/special-chars, legacy version-only table, full pg-meta-shaped multi-statement string, GUC non-leakage). Note (out of scope, pre-existing): `packages/pg-meta/src/sql/studio/advisor/lints.ts` still uses `query_to_xml` — a separate pre-existing Multigres risk that should get its own splinter-pattern sync. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Saxon Fletcher <saxonafletcher@gmail.com>
114 lines
4.0 KiB
TypeScript
114 lines
4.0 KiB
TypeScript
import { BadgeCheck } from 'lucide-react'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { Badge, Card, CardContent, cn } from 'ui'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { IntegrationDefinition } from './Integrations.constants'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
|
|
type IntegrationCardProps = IntegrationDefinition & {
|
|
isInstalled?: boolean
|
|
featured?: boolean
|
|
image?: string
|
|
}
|
|
|
|
const INTEGRATION_CARD_STYLE = cn(
|
|
'w-full h-full bg-surface-100 hover:bg-surface-200 hover:border-strong',
|
|
'border border-border rounded-md ease-out duration-200 transition-all'
|
|
)
|
|
|
|
export const IntegrationLoadingCard = () => {
|
|
return (
|
|
<div className={cn(INTEGRATION_CARD_STYLE, 'pl-5 pr-6 py-3 gap-3 inline-flex h-[110px]')}>
|
|
<div className="w-10 h-10 relative">
|
|
<ShimmeringLoader className="w-full h-full bg-foreground-light border rounded-md" />
|
|
</div>
|
|
<div className="grow basis-0 w-full flex flex-col justify-between items-start gap-y-2">
|
|
<div className="w-full flex-col justify-start items-start gap-y-1 flex">
|
|
<ShimmeringLoader className="w-3/4 py-2.5" />
|
|
<ShimmeringLoader className="w-full py-2.5" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const IntegrationCard = ({
|
|
id,
|
|
listingId,
|
|
status,
|
|
name,
|
|
icon,
|
|
description,
|
|
isInstalled,
|
|
featured = false,
|
|
image,
|
|
}: IntegrationCardProps) => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const shouldShowOfficialBadge = !listingId
|
|
|
|
if (featured) {
|
|
return (
|
|
<Link href={`/project/${project?.ref}/integrations/${id}/overview`} className="h-full">
|
|
<Card className="h-full">
|
|
{/* Full-width image/icon at the top */}
|
|
<div className="w-full h-24 bg-surface-400 rounded-t-md flex items-center justify-center overflow-hidden relative">
|
|
{image ? (
|
|
<Image
|
|
fill
|
|
src={image}
|
|
alt={`${name} integration`}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-12 h-12 text-foreground relative">
|
|
{icon({ className: 'w-full h-full text-foreground' })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<CardContent className="p-6 px-4">
|
|
<div className="flex-col justify-start items-center text-center gap-y-0.5 flex">
|
|
<h3>{name}</h3>
|
|
<p className="text-foreground-light text-sm line-clamp-3">{description}</p>
|
|
<div className="flex items-center gap-x-1 mt-4">
|
|
{status && <Badge variant="warning">{status}</Badge>}
|
|
{shouldShowOfficialBadge && <Badge>Official</Badge>}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Link href={`/project/${project?.ref}/integrations/${id}/overview`} className="h-full">
|
|
<Card className="h-full">
|
|
<CardContent className="flex flex-col p-4 @2xl:p-6 h-full">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="shrink-0 w-10 h-10 relative bg-white border rounded-md flex items-center justify-center">
|
|
{icon()}
|
|
</div>
|
|
{isInstalled && (
|
|
<div className="flex items-center gap-x-1">
|
|
<BadgeCheck size={14} className="text-brand-link" />
|
|
<span className="text-brand-link text-xs">Installed</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-col justify-start items-start gap-y-0.5 flex flex-1">
|
|
<h3 className="text-foreground text-sm">{name}</h3>
|
|
|
|
<p className="text-foreground-light text-xs flex-1">{description}</p>
|
|
<div className="flex items-center gap-x-1 mt-4">
|
|
{status && <Badge variant="warning">{status}</Badge>}
|
|
{shouldShowOfficialBadge && <Badge>Official</Badge>}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
)
|
|
}
|