From 3823186914e5c23d1a01fc7026ed04a7e5f9669b Mon Sep 17 00:00:00 2001
From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Date: Mon, 21 Sep 2026 12:27:22 +0200
Subject: [PATCH] fix(ui): hide Registry when it is unavailable to the
deployment (#12847)
---
ui/actions/registry/registry.adapter.ts | 16 ++++++-
ui/actions/registry/registry.test.ts | 44 ++++++++++++++++++-
ui/actions/registry/registry.ts | 4 ++
.../wizard/provider-wizard-modal.test.tsx | 8 +++-
.../forms/connect-account-form.test.tsx | 28 +++++++++++-
.../workflow/forms/connect-account-form.tsx | 3 ++
6 files changed, 98 insertions(+), 5 deletions(-)
diff --git a/ui/actions/registry/registry.adapter.ts b/ui/actions/registry/registry.adapter.ts
index 4ae9de0c7d..e84fab1218 100644
--- a/ui/actions/registry/registry.adapter.ts
+++ b/ui/actions/registry/registry.adapter.ts
@@ -24,6 +24,7 @@ const REGISTRY_TASK_PATH_PREFIX = "/api/v1/tasks/";
const REGISTRY_ERROR_CODE = {
KEY_REJECTED: "registry_key_rejected",
UNAVAILABLE: "registry_unavailable",
+ PAGE_NOT_FOUND: "registry_page_not_found",
} as const;
// Opposite remedies, so a 409 is never read without its code.
const REGISTRY_REMOVAL_CONFLICT_CODE = {
@@ -216,18 +217,29 @@ export async function classifyRegistryFailure(
return { status: REGISTRY_FAILURE.ERROR };
}
+// An enabled backend also answers 404 (missing page): tell them apart by code.
+export async function isRegistryDisabledResponse(response: Response) {
+ if (response.status !== 404) return false;
+ const codes = await getRegistryErrorCodes(response);
+ return !codes.includes(REGISTRY_ERROR_CODE.PAGE_NOT_FOUND);
+}
+
function isRegistryDiscoveryEndpoint(endpoint: RegistryEndpoint) {
return registryDiscoveryEndpoints.has(endpoint);
}
-async function getRegistryErrorCode(response: Response) {
+async function getRegistryErrorCodes(response: Response) {
const parsed = errorDocumentSchema.safeParse(
await response
.clone()
.json()
.catch(() => undefined),
);
- return parsed.success ? parsed.data.errors[0]?.code : undefined;
+ return parsed.success ? parsed.data.errors.map(({ code }) => code) : [];
+}
+
+async function getRegistryErrorCode(response: Response) {
+ return (await getRegistryErrorCodes(response))[0];
}
const REGISTRY_CATALOG_PAGE_SIZE = 100;
diff --git a/ui/actions/registry/registry.test.ts b/ui/actions/registry/registry.test.ts
index 7ae458cc3f..a3a44ffcf9 100644
--- a/ui/actions/registry/registry.test.ts
+++ b/ui/actions/registry/registry.test.ts
@@ -119,14 +119,17 @@ describe("installed Registry provider discovery", () => {
emptyMetadata = false,
failedEndpoint,
failureStatus = 500,
+ failureBody = {},
}: {
emptyMetadata?: boolean;
failedEndpoint?: string;
failureStatus?: number;
+ failureBody?: unknown;
} = {}) {
fetchMock.mockImplementation((url: string) => {
const endpoint = new URL(url).pathname.split("/").pop();
- if (endpoint === failedEndpoint) return jsonResponse({}, failureStatus);
+ if (endpoint === failedEndpoint)
+ return jsonResponse(failureBody, failureStatus);
if (endpoint === "available-artifacts")
return jsonResponse({
data: [
@@ -242,6 +245,45 @@ describe("installed Registry provider discovery", () => {
});
},
);
+
+ it.each(["available-artifacts", "artifacts", "providers"])(
+ "hides Registry when the backend has it disabled and %s answers 404",
+ async (failedEndpoint) => {
+ mockDiscovery({ failedEndpoint, failureStatus: 404 });
+ expect(await getInstalledRegistryProviderOptions()).toEqual({
+ status: "access_denied",
+ });
+ },
+ );
+
+ it.each(["available-artifacts", "providers"])(
+ "keeps Registry visible when an enabled backend answers 404 for a missing %s page",
+ async (failedEndpoint) => {
+ mockDiscovery({
+ failedEndpoint,
+ failureStatus: 404,
+ failureBody: {
+ errors: [{ status: "404", code: "registry_page_not_found" }],
+ },
+ });
+ expect(await getInstalledRegistryProviderOptions()).toEqual({
+ status: "error",
+ });
+ },
+ );
+
+ it("keeps Registry visible when the missing-page code is not the first error", async () => {
+ mockDiscovery({
+ failedEndpoint: "available-artifacts",
+ failureStatus: 404,
+ failureBody: {
+ errors: [{ code: "not_found" }, { code: "registry_page_not_found" }],
+ },
+ });
+ expect(await getInstalledRegistryProviderOptions()).toEqual({
+ status: "error",
+ });
+ });
});
describe("Registry guarded reads", () => {
diff --git a/ui/actions/registry/registry.ts b/ui/actions/registry/registry.ts
index 976373a965..4d470bfc6b 100644
--- a/ui/actions/registry/registry.ts
+++ b/ui/actions/registry/registry.ts
@@ -47,6 +47,7 @@ import {
classifyRegistryRemovalConflict,
collectCompleteRegistryCatalog,
isRegistryCollection,
+ isRegistryDisabledResponse,
parseRegistryArtifactSubmission,
parseRegistryCredentialSubmission,
RegistryCatalogPageError,
@@ -86,6 +87,9 @@ async function readRegistryResponse(
return { status: REGISTRY_FAILURE.ERROR };
}
if (response.ok) return response;
+ // A backend with Registry disabled answers 404: hide it like a denial.
+ if (await isRegistryDisabledResponse(response))
+ return { status: REGISTRY_FAILURE.ACCESS_DENIED };
return endpoint === REGISTRY_ENDPOINT.PROVIDERS ||
endpoint === REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS
diff --git a/ui/components/providers/wizard/provider-wizard-modal.test.tsx b/ui/components/providers/wizard/provider-wizard-modal.test.tsx
index fc913924bc..f77a866831 100644
--- a/ui/components/providers/wizard/provider-wizard-modal.test.tsx
+++ b/ui/components/providers/wizard/provider-wizard-modal.test.tsx
@@ -1,6 +1,6 @@
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Toaster } from "@/components/shadcn/toast/Toaster";
import { resetToasts } from "@/components/shadcn/toast/use-toast";
@@ -89,6 +89,8 @@ async function enterAccountDetails() {
describe("provider wizard account creation", () => {
beforeEach(() => {
+ // Registry discovery only runs in Cloud.
+ vi.stubEnv("UI_CLOUD_ENABLED", "true");
useProviderWizardStore.getState().reset();
resetToasts();
getInstalledRegistryProviderOptions.mockResolvedValue({
@@ -97,6 +99,10 @@ describe("provider wizard account creation", () => {
});
});
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
it("shows progress, blocks repeat clicks, and advances after creation", async () => {
// Given
let resolveCreation!: (value: typeof createdAccount) => void;
diff --git a/ui/components/providers/workflow/forms/connect-account-form.test.tsx b/ui/components/providers/workflow/forms/connect-account-form.test.tsx
index 2a906d431c..763cbcb4b3 100644
--- a/ui/components/providers/workflow/forms/connect-account-form.test.tsx
+++ b/ui/components/providers/workflow/forms/connect-account-form.test.tsx
@@ -1,6 +1,6 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { addProvider, updateProvider, getInstalledRegistryProviderOptions } =
vi.hoisted(() => ({
@@ -86,6 +86,32 @@ describe("provider account aliases", () => {
describe("Registry provider source tabs", () => {
beforeEach(() => {
vi.clearAllMocks();
+ vi.stubEnv("UI_CLOUD_ENABLED", "true");
+ });
+
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ it("never mentions Registry outside Cloud, even when discovery would fail", async () => {
+ // Given
+ vi.stubEnv("UI_CLOUD_ENABLED", "false");
+ getInstalledRegistryProviderOptions.mockRejectedValue(new Error("network"));
+
+ // When
+ render();
+
+ // Then
+ expect(
+ await screen.findByRole("option", { name: /Amazon Web Services/ }),
+ ).toBeVisible();
+ expect(getInstalledRegistryProviderOptions).not.toHaveBeenCalled();
+ expect(
+ screen.queryByText("Registry providers could not be loaded"),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole("tab", { name: "Registry" }),
+ ).not.toBeInTheDocument();
});
it("hides the Registry tab when discovery denies access (Local or flag off)", async () => {
diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx
index ddc880e3e7..ea83267cc0 100644
--- a/ui/components/providers/workflow/forms/connect-account-form.tsx
+++ b/ui/components/providers/workflow/forms/connect-account-form.tsx
@@ -22,6 +22,7 @@ import {
REGISTRY_PROVIDER_DISCOVERY,
type RegistryProviderOption,
} from "@/lib/registry/provider-options";
+import { isCloud } from "@/lib/shared/env";
import {
createAddProviderFormSchema,
AddProviderFormValues,
@@ -233,6 +234,8 @@ export const ConnectAccountForm = ({
const createdAccount = useRef(null);
useEffect(() => {
+ // Registry is Cloud-only: elsewhere never ask, so a failure cannot surface it.
+ if (!isCloud()) return;
let active = true;
const load = async () => {
try {