From 7f47a2fd3e3fa74e35359ed46da716100374bba9 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 14 Sep 2026 13:06:08 +0200 Subject: [PATCH] feat: label resource types and integrations with hub display names Co-Authored-By: Claude Opus 5 (1M context) --- backend/windmill-api/openapi.yaml | 7 ++ backend/windmill-store/src/resources.rs | 34 ++++++-- .../src/lib/components/ImportSetupStep.svelte | 7 ++ .../flows/pickers/PickHubScript.svelte | 2 + .../flows/pickers/PickHubScriptQuick.svelte | 6 +- .../lib/components/home/ListFilters.svelte | 3 +- .../components/home/ListFiltersQuick.svelte | 3 +- .../components/mcp/McpScopeSelector.svelte | 16 ++-- .../src/lib/components/pickerPopularity.ts | 17 +++- .../src/lib/components/resourceTypeDisplay.ts | 81 +++++++++++++++++-- .../components/resourceTypeMatchRank.test.ts | 27 +++++++ 11 files changed, 175 insertions(+), 28 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index a43135d440..1fef8f844e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -8706,6 +8706,9 @@ paths: type: string picks: type: integer + display_name: + description: the label the hub curates for the resource type, absent where it names none + type: string required: - name - app @@ -8985,6 +8988,10 @@ paths: picks: description: how often the integration has been picked, absent on a hub that does not count picks type: integer + display_name: + description: the label the hub curates for the integration, null or absent where it names none + type: string + nullable: true required: - name diff --git a/backend/windmill-store/src/resources.rs b/backend/windmill-store/src/resources.rs index 2df5dbab5f..c4c63a3365 100644 --- a/backend/windmill-store/src/resources.rs +++ b/backend/windmill-store/src/resources.rs @@ -2672,6 +2672,10 @@ struct HubResourceTypeEntry { /// fail the whole parse and take pick reporting — which needs just the id — with it. #[serde(default)] app: Option, + /// Raw: `None` where nobody named the type. The frontend derives those labels with its own + /// word casing, which a titleised guess from the hub would override. + #[serde(default)] + display_name: Option, } #[derive(Clone)] @@ -2682,6 +2686,7 @@ struct HubResourceType { /// hub knows that. Without it a workspace holding a `discord_webhook` resource looks /// like one that has never touched Discord. app: String, + display_name: Option, } /// Reads the index cache, choosing the TTL by what is stored: a failure expires far sooner @@ -2722,9 +2727,9 @@ async fn hub_resource_types( if !response.status().is_success() { return None; } - // Only the id and the app are kept. That listing carries every type's schema — - // around a megabyte — and neither reporting a pick nor grouping types by - // integration needs it. + // Only the id, the app and the display name are kept. That listing carries every + // type's schema — around a megabyte — and neither reporting a pick, grouping types by + // integration nor labelling them needs it. Some( response .json::>() @@ -2733,7 +2738,11 @@ async fn hub_resource_types( .into_iter() .map(|rt| { let app = rt.app.unwrap_or_else(|| rt.name.clone()); - (rt.name, HubResourceType { id: rt.id, app }) + let display_name = rt + .display_name + .map(|n| n.trim().to_string()) + .filter(|n| !n.is_empty()); + (rt.name, HubResourceType { id: rt.id, app, display_name }) }) .collect::>(), ) @@ -2813,7 +2822,7 @@ mod hub_picks_tests { let index = || { Some(HashMap::from([( "slack".to_string(), - HubResourceType { id: 1, app: "slack".to_string() }, + HubResourceType { id: 1, app: "slack".to_string(), display_name: None }, )])) }; @@ -2860,10 +2869,13 @@ struct HubResourceTypeInfo { /// integration rather than per type. app: String, picks: i64, + /// The label the hub curates for the type, absent where it names none. + #[serde(skip_serializing_if = "Option::is_none")] + display_name: Option, } -/// What the hub knows about its resource types: which integration each belongs to, and how -/// often each has been picked. +/// What the hub knows about its resource types: which integration each belongs to, what it +/// names each, and how often each has been picked. /// /// Empty rather than an error when the hub answers neither read, so the pickers treat an /// older or private hub as "no hub signal" and fall back to what the workspace itself uses. @@ -2917,6 +2929,7 @@ async fn list_hub_resource_type_info( picks: picks_by_name.remove(&name).unwrap_or(0), name, app: rt.app, + display_name: rt.display_name, }) .collect(); // What the index did not account for is a type the picks read knows and the listing does @@ -2925,7 +2938,12 @@ async fn list_hub_resource_type_info( info.extend( picks_by_name .into_iter() - .map(|(name, picks)| HubResourceTypeInfo { app: name.clone(), name, picks }), + .map(|(name, picks)| HubResourceTypeInfo { + app: name.clone(), + name, + picks, + display_name: None, + }), ); Ok(Json(info)) diff --git a/frontend/src/lib/components/ImportSetupStep.svelte b/frontend/src/lib/components/ImportSetupStep.svelte index 51eaf725f8..8da43728a6 100644 --- a/frontend/src/lib/components/ImportSetupStep.svelte +++ b/frontend/src/lib/components/ImportSetupStep.svelte @@ -20,6 +20,7 @@ import { OauthService } from '$lib/gen' import { registryCcCapableFor } from '$lib/components/oauthRegistry' import { resourceTypeDisplayName } from '$lib/components/resourceTypeDisplay' + import { loadHubResourceTypeDisplayNames } from '$lib/components/pickerPopularity' import { applyOneMigration } from '$lib/components/workspaceSettings/projectInstall' import { probeMigrationsApplied } from '$lib/importWizard/probe' import { @@ -197,6 +198,12 @@ * first half and Connect disappears on the eight such providers, where it would work. */ const canConnectType = (rt: string) => instanceConnects.has(rt) || registryCcCapableFor(rt) + + // Row labels read the hub's curated resource type names, which arrive after first render. + $effect(() => { + loadHubResourceTypeDisplayNames(workspace) + }) + let appConnect: AppConnectDrawer | undefined = $state(undefined) const customInstanceDbs = resource([() => workspace], SettingService.listCustomInstanceDbs) diff --git a/frontend/src/lib/components/flows/pickers/PickHubScript.svelte b/frontend/src/lib/components/flows/pickers/PickHubScript.svelte index d84228dd35..d1ed785ba3 100644 --- a/frontend/src/lib/components/flows/pickers/PickHubScript.svelte +++ b/frontend/src/lib/components/flows/pickers/PickHubScript.svelte @@ -4,6 +4,7 @@ import { capitalize } from '$lib/utils' import NoItemFound from '$lib/components/home/NoItemFound.svelte' import { APP_TO_ICON_COMPONENT } from '$lib/components/icons' + import { setHubIntegrationDisplayNames } from '$lib/components/resourceTypeDisplay' import ListFilters from '$lib/components/home/ListFilters.svelte' import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen' import { Loader2 } from 'lucide-svelte' @@ -67,6 +68,7 @@ IntegrationService.listHubIntegrations({ kind: filterKind }), $workspaceStore ? localCountsByIntegration($workspaceStore) : {} ]) + setHubIntegrationDisplayNames(integrations) const hubPicks = Object.fromEntries(integrations.map((x) => [x.name, x.picks ?? 0])) popularity = byPopularity(hubPicks, local) allApps = integrations.map((x) => x.name).sort(popularity) diff --git a/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte b/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte index d32e82c702..958c439769 100644 --- a/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte +++ b/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte @@ -1,7 +1,10 @@