mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
I made the connected-project framework icons fall back when no shipped SVG exists for a framework. The three icon sites built `/img/icons/frameworks/<framework>.svg` straight from the integration's framework preset, which is an open-ended string. They only fell back when the value was empty, so any preset without an asset (`express`, `hono`, `fastapi`, `tanstack-start` and others) showed a broken image and logged a 404. **Changed:** - **Broken framework icons**: `getFrameworkIconUrl` returns the asset URL only for slugs in a set that mirrors `public/img/icons/frameworks/`. The integration connection row, the org project linker and the marketplace project picker now show their existing fallback icon for any other slug. A test keeps the set equal to the directory listing. - **Framework type**: I deleted the hand-kept `VercelFramework` union. It listed exactly the shipped icon slugs, while the API types the field as `string | null`, and that mismatch is what made the old empty-only check look safe. **Note:** I rejected an `onError` fallback because the browser still sends the 404 request. Adding logos for common presets is left for design. ## To test Tested on Vercel preview (staging): no real connection there uses these presets, so I rewrote the org integrations response in the browser to give one integration four connections. - [x] Open an org's Integrations page with connections whose framework has no shipped icon (`express`, `eve`, `tanstack-start-lovable`). Expect the fallback badge and no request under `/dashboard/img/icons/frameworks/` for those slugs. Observed: all three rows showed the badge and the network log had no request for their SVGs. - [x] Same page with a `nextjs` connection. Expect its framework logo. Observed: `nextjs.svg` loaded with a 200. - [x] Same page with the real, unmodified response (one connection with `framework: null`). Expect the badge, no frameworks requests, and no new console errors. Observed: as expected. ## Linear - fixes GROWTH-1309 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Vercel integration and project views now display framework icons when available and fall back to the Vercel icon when no matching icon exists. * Framework metadata now supports values beyond a fixed list, while unsupported frameworks continue to use the fallback icon. * **Tests** * Added coverage for supported and unsupported framework icons, including base-path handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import { getCreateMigrationsTableSQL, getInsertMigrationSQL } from '@supabase/pg-meta'
|
|
|
|
import { isResponseOk } from './api/apiWrapper'
|
|
import { fetchHandler } from '@/data/fetchers'
|
|
import type { Integration } from '@/data/integrations/integrations.types'
|
|
import { BASE_PATH } from '@/lib/constants'
|
|
import { ResponseError, type SupaResponse } from '@/types'
|
|
|
|
async function fetchGitHub<T = any>(url: string, responseJson = true): Promise<SupaResponse<T>> {
|
|
const response = await fetchHandler(url)
|
|
if (!response.ok) {
|
|
return {
|
|
error: new ResponseError(response.statusText, response.status),
|
|
}
|
|
}
|
|
try {
|
|
return (responseJson ? await response.json() : await response.text()) as T
|
|
} catch (error: any) {
|
|
return {
|
|
error: new ResponseError(error.message, 500),
|
|
}
|
|
}
|
|
}
|
|
|
|
export type File = {
|
|
name: string
|
|
download_url: string
|
|
}
|
|
|
|
/**
|
|
* Returns the initial migration SQL from a GitHub repo.
|
|
* @param externalId An external GitHub URL for example: https://github.com/vercel/next.js/tree/canary/examples/with-supabase
|
|
*/
|
|
export async function getInitialMigrationSQLFromGitHubRepo(
|
|
externalId?: string
|
|
): Promise<string | null> {
|
|
if (!externalId) return null
|
|
|
|
const [, , , owner, repo, , branch, ...pathSegments] = externalId?.split('/') ?? []
|
|
const path = pathSegments.join('/')
|
|
|
|
const baseGitHubUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`
|
|
const supabaseFolderUrl = `${baseGitHubUrl}/supabase?ref=${branch}`
|
|
const supabaseMigrationsPath = `supabase/migrations` // TODO: read this from the `supabase/config.toml` file
|
|
const migrationsFolderUrl = `${baseGitHubUrl}/${supabaseMigrationsPath}${
|
|
branch ? `?ref=${branch}` : ``
|
|
}`
|
|
|
|
const [supabaseFilesResponse, migrationFilesResponse] = await Promise.all([
|
|
fetchGitHub<File[]>(supabaseFolderUrl),
|
|
fetchGitHub<File[]>(migrationsFolderUrl),
|
|
])
|
|
|
|
if (!isResponseOk(supabaseFilesResponse)) {
|
|
console.warn(`Failed to fetch supabase files from GitHub: ${supabaseFilesResponse.error}`)
|
|
return null
|
|
}
|
|
if (!isResponseOk(migrationFilesResponse)) {
|
|
console.warn(`Failed to fetch migration files from GitHub: ${migrationFilesResponse.error}`)
|
|
return null
|
|
}
|
|
|
|
const seedFileUrl = supabaseFilesResponse.find((file) => file.name === 'seed.sql')?.download_url
|
|
const sortedFiles = migrationFilesResponse.sort((a, b) => {
|
|
// sort by name ascending
|
|
if (a.name < b.name) return -1
|
|
if (a.name > b.name) return 1
|
|
return 0
|
|
})
|
|
const migrationFileDownloadUrlPromises = sortedFiles.map((file) =>
|
|
fetchGitHub<string>(file.download_url, false)
|
|
)
|
|
|
|
const [seedFileResponse, ...migrationFileResponses] = await Promise.all([
|
|
seedFileUrl ? fetchGitHub<string>(seedFileUrl, false) : Promise.resolve<string>(''),
|
|
...migrationFileDownloadUrlPromises,
|
|
])
|
|
|
|
const migrations = migrationFileResponses.filter((response) => isResponseOk(response)).join(';')
|
|
const seed = isResponseOk(seedFileResponse) ? seedFileResponse : ''
|
|
|
|
const createMigrationsTableSql = getCreateMigrationsTableSQL()
|
|
|
|
const migrationsTableSql = `
|
|
${createMigrationsTableSql}
|
|
${sortedFiles
|
|
.map((file, i) => {
|
|
const migration = migrationFileResponses[i]
|
|
if (!isResponseOk(migration)) return ''
|
|
|
|
const version = file.name.split('_')[0]
|
|
const statements = JSON.stringify(
|
|
migration
|
|
.split(';')
|
|
.map((statement) => statement.trim())
|
|
.filter(Boolean)
|
|
)
|
|
return getInsertMigrationSQL({ name: file.name, version, statements })
|
|
})
|
|
.join('')}
|
|
`
|
|
|
|
return `${migrations};${migrationsTableSql};${seed}`
|
|
}
|
|
|
|
type VercelIntegration = Extract<Integration, { integration: { name: 'Vercel' } }>
|
|
type GitHubIntegration = Extract<Integration, { integration: { name: 'GitHub' } }>
|
|
|
|
export function getIntegrationConfigurationUrl(integration: Integration) {
|
|
if (integration.integration.name === 'Vercel') {
|
|
return getVercelConfigurationUrl(integration as VercelIntegration)
|
|
}
|
|
|
|
if (integration.integration.name === 'GitHub') {
|
|
return getGitHubConfigurationUrl(integration as GitHubIntegration)
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
function getVercelConfigurationUrl(integration: VercelIntegration) {
|
|
return `https://vercel.com/dashboard/${
|
|
integration.metadata?.account.type === 'Team'
|
|
? `${integration.metadata?.account.team_slug}/`
|
|
: ''
|
|
}integrations/${integration.metadata?.configuration_id}`
|
|
}
|
|
|
|
function getGitHubConfigurationUrl(integration: GitHubIntegration) {
|
|
return `https://github.com/${
|
|
integration.metadata?.account.type === 'Organization'
|
|
? `organizations/${integration.metadata?.account.name}/`
|
|
: ''
|
|
}settings/installations/${integration.metadata?.installation_id}`
|
|
}
|
|
|
|
export const FRAMEWORK_ICON_SLUGS: ReadonlySet<string> = new Set([
|
|
'angular',
|
|
'astro',
|
|
'blitzjs',
|
|
'brunch',
|
|
'create-react-app',
|
|
'docusaurus',
|
|
'docusaurus-2',
|
|
'dojo',
|
|
'eleventy',
|
|
'ember',
|
|
'gatsby',
|
|
'gridsome',
|
|
'hexo',
|
|
'hugo',
|
|
'hydrogen',
|
|
'ionic-angular',
|
|
'ionic-react',
|
|
'jekyll',
|
|
'middleman',
|
|
'nextjs',
|
|
'nuxtjs',
|
|
'parcel',
|
|
'polymer',
|
|
'preact',
|
|
'redwoodjs',
|
|
'remix',
|
|
'saber',
|
|
'sanity',
|
|
'sapper',
|
|
'scully',
|
|
'solidstart',
|
|
'stencil',
|
|
'storybook',
|
|
'svelte',
|
|
'sveltekit',
|
|
'sveltekit-1',
|
|
'umijs',
|
|
'vite',
|
|
'vitepress',
|
|
'vue',
|
|
'vuepress',
|
|
'zola',
|
|
])
|
|
|
|
export function getFrameworkIconUrl(framework?: string | null): string | undefined {
|
|
if (!framework || !FRAMEWORK_ICON_SLUGS.has(framework)) return undefined
|
|
return `${BASE_PATH}/img/icons/frameworks/${framework}.svg`
|
|
}
|