Files
supabase/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx
Joshen Lim f2e20eac34 Joshen/fe 3651 deprecate monacoeditor from grid folder to use codeeditor (#47179)
## Context

Part of efforts to consolidate all the code editors that we have in the
repository

`CodeEditor` will serve as the base monaco editor file that all UIs
should consume from
It's aimed to be generic and just stores the common logic that will be
generally used where-ever we need a code editor (editor options, base
editor set up on mount, etc)

The idea is that `CodeEditor` holds just 3 default actions (run queyr,
format document and placeholder fill)
If any editor needs specific behaviours (e.g SQL Editor), they can
declare them in the `onMount` prop of `CodeEditor` which gives some
flexibility

## Changes involved
- Use `CodeEditor` component for SQL Editor's `MonacoEditor`
- Shifted Cmd K behaviour into `CodeEditor` since that's probably needed
everywhere that we render that UI
- Deprecate `MonacoEditor` from the table editor's `grid` folder
- All files that were using that component to use `CodeEditor` component
instead

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Migrated the studio’s code editing UI (including JSON, text, and
payload viewers) to a unified CodeEditor experience.
* **New Features**
  * Added plaintext language support for read-only/truncated views.
* **Behavior Changes**
* Improved editor startup by setting cursor position consistently and
deferring autofocus.
* Streamlined editor context-menu actions to the core set (run query,
format, placeholder fill).
* Updated SQL editor wiring for more consistent command/menu and
selection handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-23 11:39:24 +08:00

239 lines
8.8 KiB
TypeScript

import { SupportCategories } from '@supabase/shared-types/out/constants'
import { Search } from 'lucide-react'
import { useRef, useState } from 'react'
import {
Button,
Card,
cn,
SidePanel,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { Admonition, TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { MigrationsEmptyState } from './MigrationsEmptyState'
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { InlineLink } from '@/components/ui/InlineLink'
import { DatabaseMigration, useMigrationsQuery } from '@/data/database/migrations-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import { formatMigrationVersionLabel, parseMigrationVersion } from '@/lib/migration-utils'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
const Migrations = () => {
const [search, setSearch] = useState('')
const [selectedMigration, setSelectedMigration] = useState<DatabaseMigration>()
const searchInputRef = useRef<HTMLInputElement>(null)
useShortcut(
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
() => {
searchInputRef.current?.focus()
searchInputRef.current?.select()
},
{ label: 'Search migrations' }
)
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setSearch(''))
const { data: project } = useSelectedProjectQuery()
const {
data = [],
isPending: isLoading,
isSuccess,
isError,
error,
} = useMigrationsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const migrations =
search.length === 0
? data
: (data.filter(
(migration) => migration.version.includes(search) || migration.name?.includes(search)
) ?? [])
return (
<>
{isLoading && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
<div>
{isError && (
<Admonition
type="warning"
title="Failed to retrieve migration history for database"
description={
<>
<p className="mb-1">
Try refreshing your browser, but if the issue persists for more than a few
minutes, please reach out to us via support.
</p>
<p className="mb-4">Error: {error?.message ?? 'Unknown'}</p>
</>
}
>
<Button key="contact-support" asChild variant="default">
<SupportLink
queryParams={{
projectRef: project?.ref,
category: SupportCategories.DASHBOARD_BUG,
subject: 'Unable to view database migrations',
}}
>
Contact support
</SupportLink>
</Button>
</Admonition>
)}
{isSuccess && (
<div>
{data.length <= 0 && <MigrationsEmptyState />}
{data.length > 0 && (
<div className="flex flex-col gap-y-4">
<Input
ref={searchInputRef}
size="tiny"
placeholder="Search for a migration"
value={search}
className="w-full lg:w-52"
onChange={(e: any) => setSearch(e.target.value)}
icon={<Search />}
/>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="version" style={{ width: '180px' }}>
Version
</TableHead>
<TableHead key="name">Name</TableHead>
<TableHead key="insertedAt">Inserted at (UTC)</TableHead>
<TableHead key="buttons" />
</TableRow>
</TableHeader>
<TableBody>
{migrations.length > 0 ? (
migrations.map((migration) => {
const versionDayjs = parseMigrationVersion(migration.version)
const label = formatMigrationVersionLabel(migration.version)
const insertedAt = versionDayjs ? versionDayjs.toISOString() : undefined
return (
<TableRow key={migration.version}>
<TableCell>{migration.version}</TableCell>
<TableCell
className={cn(
(migration?.name ?? '').length === 0 && 'text-foreground-lighter!'
)}
>
{migration?.name ?? 'Name not available'}
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger>
{!!insertedAt ? (
<TimestampInfo
className="text-sm"
label={label}
utcTimestamp={insertedAt}
/>
) : (
<p className="text-foreground-lighter">Unknown</p>
)}
</TooltipTrigger>
{!insertedAt && (
<TooltipContent side="right" className="w-64 text-center">
This migration was not generated via the{' '}
<InlineLink
href={`${DOCS_URL}/guides/deployment/database-migrations`}
>
Supabase CLI
</InlineLink>{' '}
and hence we're unable to parse when this migration was
inserted at.
</TooltipContent>
)}
</Tooltip>
</TableCell>
<TableCell align="right">
<Button
variant="default"
onClick={() => setSelectedMigration(migration)}
>
View migration SQL
</Button>
</TableCell>
</TableRow>
)
})
) : (
<TableRow>
<TableCell colSpan={3}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{search}" did not return any results
</p>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
)}
</div>
)}
</div>
<SidePanel
size="large"
visible={selectedMigration !== undefined}
header={`Migration: ${selectedMigration?.version}`}
onCancel={() => setSelectedMigration(undefined)}
customFooter={
<div className="flex items-center justify-end p-4 border-t border-overlay-border">
<Button variant="default" onClick={() => setSelectedMigration(undefined)}>
Close
</Button>
</div>
}
>
<div className="h-full">
<div className="relative h-full">
<CodeEditor
isReadOnly
id={selectedMigration?.version ?? ''}
language="pgsql"
defaultValue={
selectedMigration?.statements?.join(';\n') +
(selectedMigration?.statements?.length ? ';' : '')
}
/>
</div>
</div>
</SidePanel>
</>
)
}
export default Migrations