fix(ui): hide Registry when it is unavailable to the deployment (#12847)

This commit is contained in:
Alejandro Bailo
2026-09-21 12:27:22 +02:00
committed by GitHub
parent 03cb59c20d
commit 3823186914
6 changed files with 98 additions and 5 deletions
+14 -2
View File
@@ -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;
+43 -1
View File
@@ -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", () => {
+4
View File
@@ -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
@@ -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;
@@ -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(<ConnectAccountForm onSuccess={vi.fn()} />);
// 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 () => {
@@ -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<ConnectAccountSuccessData | null>(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 {