mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): fall back for framework icons without an asset (#51065)
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 -->
This commit is contained in:
1 parent
9690efeb42
commit
38b74af3f1
7 files changed
+127
-72
No files matched your search
+39
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { IntegrationConnection } from './IntegrationPanels'
|
||||
import type { IntegrationProjectConnection } from '@/data/integrations/integrations.types'
|
||||
import { render } from '@/tests/helpers'
|
||||
|
||||
vi.mock('@/data/projects/project-detail-query', () => ({
|
||||
useProjectDetailQuery: () => ({ data: undefined, isPending: false }),
|
||||
}))
|
||||
|
||||
const connectionWithFramework = (framework: string) =>
|
||||
({
|
||||
id: 'connection-1',
|
||||
supabase_project_ref: 'project-ref',
|
||||
foreign_project_id: 'vercel-project-1',
|
||||
metadata: { id: 'vercel-project-1', name: 'my-app', framework },
|
||||
}) as IntegrationProjectConnection
|
||||
|
||||
const frameworkIconSelector = 'img[src*="/img/icons/frameworks/"]'
|
||||
|
||||
describe('IntegrationConnection', () => {
|
||||
it('renders the fallback badge for a framework without a shipped icon', () => {
|
||||
const { container } = render(
|
||||
<IntegrationConnection type="Vercel" connection={connectionWithFramework('express')} />
|
||||
)
|
||||
|
||||
expect(container.querySelector(frameworkIconSelector)).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the framework icon for a framework with a shipped icon', () => {
|
||||
const { container } = render(
|
||||
<IntegrationConnection type="Vercel" connection={connectionWithFramework('nextjs')} />
|
||||
)
|
||||
|
||||
expect(container.querySelector(frameworkIconSelector)?.getAttribute('src')).toMatch(
|
||||
/\/img\/icons\/frameworks\/nextjs\.svg$/
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from '@/data/integrations/integrations.types'
|
||||
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
|
||||
import { getFrameworkIconUrl, getIntegrationConfigurationUrl } from '@/lib/integration-utils'
|
||||
|
||||
const ICON_STROKE_WIDTH = 2
|
||||
const ICON_SIZE = 14
|
||||
@@ -150,6 +150,7 @@ export const IntegrationConnection = forwardRef<HTMLLIElement, IntegrationConnec
|
||||
ref
|
||||
) => {
|
||||
const { data: project } = useProjectDetailQuery({ ref: connection.supabase_project_ref })
|
||||
const frameworkIconUrl = getFrameworkIconUrl(connection?.metadata?.framework)
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -186,17 +187,12 @@ export const IntegrationConnection = forwardRef<HTMLLIElement, IntegrationConnec
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0 flex gap-2 items-center">
|
||||
{!connection?.metadata?.framework ? (
|
||||
{!frameworkIconUrl ? (
|
||||
<div className="bg-black text-white w-4 h-4 rounded-sm flex items-center justify-center">
|
||||
<HandleIcon type={type} className={'w-2.5!'} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${connection.metadata.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
/>
|
||||
<img src={frameworkIconUrl} width={21} height={21} alt={`icon`} />
|
||||
)}
|
||||
{type === 'GitHub' ? (
|
||||
<a
|
||||
|
||||
+4
-8
@@ -13,7 +13,7 @@ import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integratio
|
||||
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { getFrameworkIconUrl } from '@/lib/integration-utils'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
import { useSidePanelsStateSnapshot } from '@/state/side-panels'
|
||||
import type { ResponseError } from '@/types'
|
||||
@@ -57,16 +57,12 @@ export const SidePanelVercelProjectLinker = () => {
|
||||
const getForeignProjectIcon = useCallback(
|
||||
(_project: ForeignProject) => {
|
||||
const project = vercelProjectsById[_project.id]
|
||||
const frameworkIconUrl = getFrameworkIconUrl(project?.framework)
|
||||
|
||||
return !project?.framework ? (
|
||||
return !frameworkIconUrl ? (
|
||||
vercelIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
/>
|
||||
<img src={frameworkIconUrl} width={21} height={21} alt={`icon`} />
|
||||
)
|
||||
},
|
||||
[vercelProjectsById]
|
||||
|
||||
@@ -1,50 +1,3 @@
|
||||
export type VercelFramework =
|
||||
| (
|
||||
| 'blitzjs'
|
||||
| 'nextjs'
|
||||
| 'gatsby'
|
||||
| 'remix'
|
||||
| 'astro'
|
||||
| 'hexo'
|
||||
| 'eleventy'
|
||||
| 'docusaurus-2'
|
||||
| 'docusaurus'
|
||||
| 'preact'
|
||||
| 'solidstart'
|
||||
| 'dojo'
|
||||
| 'ember'
|
||||
| 'vue'
|
||||
| 'scully'
|
||||
| 'ionic-angular'
|
||||
| 'angular'
|
||||
| 'polymer'
|
||||
| 'svelte'
|
||||
| 'sveltekit'
|
||||
| 'sveltekit-1'
|
||||
| 'ionic-react'
|
||||
| 'create-react-app'
|
||||
| 'gridsome'
|
||||
| 'umijs'
|
||||
| 'sapper'
|
||||
| 'saber'
|
||||
| 'stencil'
|
||||
| 'nuxtjs'
|
||||
| 'redwoodjs'
|
||||
| 'hugo'
|
||||
| 'jekyll'
|
||||
| 'brunch'
|
||||
| 'middleman'
|
||||
| 'zola'
|
||||
| 'hydrogen'
|
||||
| 'vite'
|
||||
| 'vitepress'
|
||||
| 'vuepress'
|
||||
| 'parcel'
|
||||
| 'sanity'
|
||||
| 'storybook'
|
||||
)
|
||||
| null
|
||||
|
||||
export type VercelGitLink =
|
||||
| {
|
||||
/**
|
||||
@@ -142,7 +95,7 @@ export type Imetadata = {
|
||||
}
|
||||
link?: VercelGitLink
|
||||
name: string
|
||||
framework: VercelFramework
|
||||
framework: string | null
|
||||
}
|
||||
|
||||
export type IntegrationProjectConnection = {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
FRAMEWORK_ICON_SLUGS,
|
||||
getFrameworkIconUrl,
|
||||
getInitialMigrationSQLFromGitHubRepo,
|
||||
getIntegrationConfigurationUrl,
|
||||
} from './integration-utils'
|
||||
@@ -10,6 +14,7 @@ import type {
|
||||
VercelAccount,
|
||||
VercelTeamAccount,
|
||||
} from '@/data/integrations/integrations.types'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
|
||||
vi.mock('@/data/fetchers', () => ({
|
||||
fetchHandler: vi.fn(),
|
||||
@@ -255,4 +260,23 @@ describe('integration-utils', () => {
|
||||
expect(result).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFrameworkIconUrl', () => {
|
||||
it('returns undefined for missing or unshipped frameworks and a URL for shipped ones', () => {
|
||||
expect(getFrameworkIconUrl('not-a-framework')).toBeUndefined()
|
||||
expect(getFrameworkIconUrl(null)).toBeUndefined()
|
||||
expect(getFrameworkIconUrl(undefined)).toBeUndefined()
|
||||
expect(getFrameworkIconUrl('')).toBeUndefined()
|
||||
expect(getFrameworkIconUrl('nextjs')).toBe(`${BASE_PATH}/img/icons/frameworks/nextjs.svg`)
|
||||
})
|
||||
|
||||
it('stays in sync with public/img/icons/frameworks/*.svg', () => {
|
||||
const slugsOnDisk = readdirSync(join(__dirname, '../public/img/icons/frameworks'))
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.map((file) => file.replace(/\.svg$/, ''))
|
||||
.sort()
|
||||
|
||||
expect([...FRAMEWORK_ICON_SLUGS].sort()).toEqual(slugsOnDisk)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { getCreateMigrationsTableSQL, getInsertMigrationSQL } from '@supabase/pg
|
||||
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>> {
|
||||
@@ -132,3 +133,53 @@ function getGitHubConfigurationUrl(integration: GitHubIntegration) {
|
||||
: ''
|
||||
}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`
|
||||
}
|
||||
@@ -25,8 +25,8 @@ import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integratio
|
||||
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
|
||||
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { getErrorMessage } from '@/lib/get-error-message'
|
||||
import { getFrameworkIconUrl } from '@/lib/integration-utils'
|
||||
import { hasVercelDeployButtonSignals } from '@/lib/integrations/vercel-install.utils'
|
||||
import { buildStudioPageTitle } from '@/lib/page-title'
|
||||
import { useProfileNameAndPicture } from '@/lib/profile'
|
||||
@@ -83,16 +83,12 @@ const VercelChooseProjectPage: NextPageWithLayout = () => {
|
||||
const getForeignProjectIcon = useCallback(
|
||||
(_project: ForeignProject) => {
|
||||
const project = vercelProjectsById[_project.id]
|
||||
const frameworkIconUrl = getFrameworkIconUrl(project?.framework)
|
||||
|
||||
return !project?.framework ? (
|
||||
return !frameworkIconUrl ? (
|
||||
vercelIcon
|
||||
) : (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt="Framework icon"
|
||||
/>
|
||||
<img src={frameworkIconUrl} width={21} height={21} alt="Framework icon" />
|
||||
)
|
||||
},
|
||||
[vercelProjectsById]
|
||||
|
||||
Reference in new issue
Block a user