mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 16:02:24 +00:00
refactor: draw the project card's icons from the ones we already ship
The card fetched each integration icon from the hub as SVG markup, sanitized
it and injected it with `{@html}`. The hub renders those icons out of
`@windmill-labs/components` — this frontend's own package — so it was a
cross-origin round trip to get our own assets back, and it made the card
depend on a read that a hub with `API_SECRET` set refuses outright.
`hubAppIcon` resolves them through `appIconComponent` instead, so they are
components again: no fetch, no DOMPurify, no `{@html}`, and they paint on
first render rather than after a round trip. Integration icons now show even
against a gated hub; only the summary and the uploaded logo still need it.
The one thing the hub was doing for us was resolving `postgres` to the
`postgresql` mark, which its `aliasApp` bridges and our icon map does not —
so that single alias comes along, next to a note pointing at its counterpart.
`ImportProjectSummary.hub` goes with it: it existed to build icon URLs and
nothing read it afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,17 +10,14 @@
|
||||
logoUrl?: string
|
||||
/** Integration slugs to draw, most representative first. */
|
||||
iconApps: string[]
|
||||
/** Where the icons and the logo are fetched from. */
|
||||
hub: string
|
||||
counts: { apps: number; flows: number; scripts: number; resources: number }
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { resource } from 'runed'
|
||||
import { ExternalLink, LayoutGrid } from 'lucide-svelte'
|
||||
import ProjectContentBadges from '$lib/components/ProjectContentBadges.svelte'
|
||||
import { fetchAppIcon } from '$lib/hubProject'
|
||||
import { hubAppIcon } from '$lib/hubProject'
|
||||
|
||||
interface Props {
|
||||
project: ImportProjectSummary
|
||||
@@ -35,20 +32,15 @@
|
||||
// scheme the page itself was served over.
|
||||
const hubProjectUrl = $derived(`//${hubHost}/projects/${project.slug}`)
|
||||
|
||||
// Icons come from the hub rather than a local map: it ships ~270 of them and is
|
||||
// the thing that knows which slug an integration is filed under. They arrive as
|
||||
// SVG markup so `fill="currentColor"` still resolves against this page.
|
||||
// Keyed on a string so "same icons" is an equality the resource can see; the
|
||||
// fetcher reads `project`, which the key changes with.
|
||||
const iconKey = $derived(`${project.hub}|${project.iconApps.slice(0, 4).join(',')}`)
|
||||
const iconResource = resource(
|
||||
() => iconKey,
|
||||
async () =>
|
||||
(
|
||||
await Promise.all(project.iconApps.slice(0, 4).map((a) => fetchAppIcon(project.hub, a)))
|
||||
).filter((s): s is string => !!s)
|
||||
// Resolved locally rather than fetched: these are Windmill's own bundled icons, so the
|
||||
// card draws them synchronously instead of waiting on the hub — and keeps working on a
|
||||
// hub that refuses uncredentialed reads.
|
||||
const icons = $derived(
|
||||
project.iconApps
|
||||
.slice(0, 4)
|
||||
.map(hubAppIcon)
|
||||
.filter((c): c is NonNullable<typeof c> => !!c)
|
||||
)
|
||||
const icons = $derived(iconResource.current ?? [])
|
||||
|
||||
// The icon row shows the integrations the tile is not already showing: with an
|
||||
// uploaded logo the tile shows none of them, so the row shows them all.
|
||||
@@ -66,10 +58,10 @@
|
||||
{#if project.logoUrl}
|
||||
<img src={project.logoUrl} alt="" class="max-h-10 max-w-10 object-contain" />
|
||||
{:else if icons[0]}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<span class="inline-flex h-7 w-7 text-primary [&>svg]:h-full [&>svg]:w-full"
|
||||
>{@html icons[0]}</span
|
||||
>
|
||||
{@const Icon = icons[0]}
|
||||
<span class="inline-flex h-7 w-7 text-primary [&>svg]:h-full [&>svg]:w-full">
|
||||
<Icon size={28} />
|
||||
</span>
|
||||
{:else}
|
||||
<LayoutGrid size={22} class="text-secondary" />
|
||||
{/if}
|
||||
@@ -107,11 +99,10 @@
|
||||
<!-- The integrations, minus whichever one is already standing in as the logo. -->
|
||||
{#if restIcons.length > 0}
|
||||
<div class="flex shrink-0 items-center gap-1.5 pt-0.5">
|
||||
{#each restIcons as svg, i (i)}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<span class="inline-flex h-4 w-4 text-primary opacity-80 [&>svg]:h-full [&>svg]:w-full"
|
||||
>{@html svg}</span
|
||||
>
|
||||
{#each restIcons as Icon, i (i)}
|
||||
<span class="inline-flex h-4 w-4 text-primary opacity-80 [&>svg]:h-full [&>svg]:w-full">
|
||||
<Icon size={16} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import type { Component } from 'svelte'
|
||||
import { appIconComponent } from '$lib/components/icons'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
|
||||
import type { ImportProjectSummary } from '$lib/components/ImportProjectCard.svelte'
|
||||
@@ -58,7 +59,6 @@ export async function fetchHubProject(slug: string): Promise<ImportProjectSummar
|
||||
iconApps: [p.logoApp, ...(p.apps ?? [])].filter(
|
||||
(a, i, all): a is string => !!a && all.indexOf(a) === i
|
||||
),
|
||||
hub,
|
||||
counts: {
|
||||
apps: p.counts?.apps ?? 0,
|
||||
flows: p.counts?.flows ?? 0,
|
||||
@@ -86,18 +86,20 @@ export async function fetchHubProject(slug: string): Promise<ImportProjectSummar
|
||||
* including moving or hiding the wizard's own Import and Delete controls — and
|
||||
* `<image href="https://…">` is a page-view beacon pointed at whoever it likes.
|
||||
*/
|
||||
export async function fetchAppIcon(hub: string, app: string): Promise<string | undefined> {
|
||||
try {
|
||||
const res = await fetch(`${hub}/icons/integrations/${encodeURIComponent(app)}.svg`)
|
||||
if (!res.ok) return undefined
|
||||
const raw = (await res.text()).trim()
|
||||
if (!raw.startsWith('<svg')) return undefined
|
||||
const clean = DOMPurify.sanitize(raw, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true },
|
||||
FORBID_TAGS: ['style', 'image']
|
||||
}).trim()
|
||||
return clean.startsWith('<svg') ? clean : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
/**
|
||||
* The icon for a hub integration slug, resolved from the icons Windmill already bundles.
|
||||
*
|
||||
* Not fetched from the hub: the hub renders these out of `@windmill-labs/components`, which
|
||||
* is this frontend's own package, so asking it over HTTP is a round trip to get our own
|
||||
* assets back — and it made the card depend on a cross-origin request that an `API_SECRET`
|
||||
* hub refuses anyway.
|
||||
*
|
||||
* The alias exists because the two repos disagree on one slug: the hub files Postgres scripts
|
||||
* under `postgres`, the icon set ships the mark as `postgresql`. The hub bridges it in
|
||||
* `aliasApp`; this is the same bridge on the consuming side.
|
||||
*/
|
||||
const HUB_APP_ICON_ALIAS: Record<string, string> = { postgres: 'postgresql' }
|
||||
|
||||
export function hubAppIcon(app: string): Component | undefined {
|
||||
return appIconComponent(HUB_APP_ICON_ALIAS[app] ?? app)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user