Files
supabase/apps/studio/components/interfaces/Database/Schemas/useExportSchemaToImage.ts
T
Gildas Garcia 1a0fc71151 fix: improve performances on large schema image export (#45042)
## Problem

When users export a large schema, the UI becomes unresponsive for a long
time.
This is because the underlying `html-to-image` library calls
`getComputedStyle` for every node.

## Solution

- Upgrade `html-to-image` to its latest version
- Use the new `includeStyleProperties` property to call
`getComputedStyle` only once
- Extract the image export logic into a new hook

## How to test

- Open
https://studio-staging-git-gildasgarcia-fe-2998-suggest-e7fb9e-supabase.vercel.app/dashboard/project/pdmusqfyrsascxykhlge/database/schemas?schema=auth
- Rearrange tables so that they are all visible
- Export the schema as png
- It should takes (~10-15secs)

- Do the same in this PR preview
https://studio-staging-gy13zepyf-supabase.vercel.app/dashboard/project/pdmusqfyrsascxykhlge/database/schemas?schema=auth
- It should takes ~3-5secs



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

* **Improvements**
* Improved schema export: more reliable PNG/SVG exports that better
preserve visual styling, show progress state during downloads, and
surface success/error notifications.
* **Chores**
* Updated image-export library to a newer version for improved
compatibility and performance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-20 16:12:37 +02:00

91 lines
2.8 KiB
TypeScript

import { toPng, toSvg } from 'html-to-image'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
export const useExportSchemaToImage = () => {
const [isDownloading, setIsDownloading] = useState(false)
// By doing this once and passing the result to html-to-image options, we avoid html-to-image calculating it for every node.
// This improves performance a lot. See https://github.com/bubkoo/html-to-image/issues/542#issuecomment-3249408793
const allPropertyNames = useMemo(() => getAllPropertyNames(), [])
const exportSchemaToImage = useStaticEffectEvent(
async ({
element,
projectRef,
x,
y,
zoom,
format,
}: {
element: HTMLElement
projectRef: string
x: number
y: number
zoom: number
format: 'svg' | 'png'
}) => {
setIsDownloading(true)
const width = element.clientWidth
const height = element.clientHeight
const options = {
includeStyleProperties: allPropertyNames,
backgroundColor: 'white',
width,
height,
style: {
width: width.toString(),
height: height.toString(),
transform: `translate(${x}px, ${y}px) scale(${zoom})`,
},
}
try {
if (format === 'svg') {
const data = await toSvg(element, options)
const a = document.createElement('a')
a.setAttribute('download', `supabase-schema-${projectRef}.svg`)
a.setAttribute('href', data)
a.click()
toast.success('Successfully downloaded as SVG')
} else if (format === 'png') {
const data = await toPng(element, options)
const a = document.createElement('a')
a.setAttribute('download', `supabase-schema-${projectRef}.png`)
a.setAttribute('href', data)
a.click()
toast.success('Successfully downloaded as PNG')
}
} catch (error) {
console.error('Failed to download:', error)
toast.error(`Failed to download current view: ${(error as Error).message}`)
} finally {
setIsDownloading(false)
}
}
)
return useMemo(
() => ({ isDownloading, exportSchemaToImage }),
[isDownloading, exportSchemaToImage]
)
}
// Get all property names accessible through getComputedStyle(), excluding custom properties
export const getAllPropertyNames = () => {
if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') {
return []
}
const names = []
const style = getComputedStyle(document.documentElement)
for (let i = 0; i < style.length; i++) {
const name = style[i]
if (!name.startsWith('--')) {
names.push(name)
}
}
return names
}