diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 0ce3b21086..5e13450e4c 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -25624,7 +25624,8 @@ paths: Forwards to the configured Hub's public project catalogue and returns its status code and raw response body. Readable by any workspace member: the listing is not workspace-scoped, and it is proxied only because the Hub's - listing endpoint sends no CORS header. + listing endpoint sends no CORS header. Refused with 400 when the instance + has the Hub disabled, in which case no outbound request is made. operationId: listHubProjects tags: - hubPublish @@ -25637,6 +25638,8 @@ paths: text/plain: schema: type: string + "400": + description: the Hub is disabled on this instance /w/{workspace}/hub/project: get: diff --git a/backend/windmill-api/src/hub_publish.rs b/backend/windmill-api/src/hub_publish.rs index 93445a2350..633ad88a34 100644 --- a/backend/windmill-api/src/hub_publish.rs +++ b/backend/windmill-api/src/hub_publish.rs @@ -550,16 +550,6 @@ async fn get_project_by_source(ctx: HubPublishCtx) -> Result bool { } } +// The hub's project catalogue. Read by any workspace member rather than through +// `HubPublishCtx`, which requires an admin: nothing here is workspace-scoped or +// publishing-related. It exists at all because the hub's listing endpoint sends no +// CORS header, so the browser cannot read it directly the way it reads a single +// project. `accept: application/json` is what makes the hub answer with JSON. +// +// The caller's token is sent only to a hub this instance was pointed at deliberately. +// Every other route here is admin-only; this one is not, so forwarding a member's +// bearer token to `hub.windmill.dev` would put a credential replayable against this +// instance on a host outside it — for a listing that needs no credential at all. async fn list_projects( _authed: ApiAuthed, Extension(db): Extension, diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index 60cf3c55a8..f56578dcad 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -145,12 +145,16 @@ // previous controller but `fetchHubProject` takes no signal, and nothing orders the // responses — so picking A, dismissing, then picking B can land A's name, author and // counts over an import that writes B. + // Kept in a local rather than read back off `detail.current` inside `detail`'s own fetcher, + // which makes the resource's type circular and resolves it to `any`. + let lastDetail = $state(undefined) const detail = resource( () => slug, async (s) => { if (!s) return undefined const fetched = await fetchHubProject(s) - return s === slug ? fetched : detail.current + if (s === slug) lastDetail = fetched + return lastDetail } ) let project = $derived( diff --git a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte index 50a29a7714..ce960925eb 100644 --- a/frontend/src/lib/components/home/WorkspaceEmptyState.svelte +++ b/frontend/src/lib/components/home/WorkspaceEmptyState.svelte @@ -40,7 +40,9 @@ let newLinkEl: HTMLButtonElement | undefined = $state(undefined) onMount(() => { - logFeatureUsage('home', 'empty_state_view') + // Only the empty case: the counter answers how many workspaces sit empty and what + // their owners do next, and a workspace whose items are all archived is neither. + if (!archivedOnly) logFeatureUsage('home', 'empty_state_view') }) // The catalogue is fetched when the picker opens, never on render: `disable_hub` says an