diff --git a/ui/components/providers/radio-group-provider.test.tsx b/ui/components/providers/radio-group-provider.test.tsx index 34987d5a41..ae9e64b5a0 100644 --- a/ui/components/providers/radio-group-provider.test.tsx +++ b/ui/components/providers/radio-group-provider.test.tsx @@ -9,8 +9,10 @@ import type { AddProviderFormValues } from "@/types/formSchemas"; import { RadioGroupProvider } from "./radio-group-provider"; function Selector({ + registryAvailable = true, registryOptions = [{ type: "acme", label: "Acme Cloud" }], }: { + registryAvailable?: boolean; registryOptions?: RegistryProviderOption[]; }) { const form = useForm(); @@ -18,6 +20,7 @@ function Selector({ ); @@ -131,4 +134,53 @@ describe("provider selector", () => { screen.getByRole("option", { name: /Amazon Web Services/ }), ).toBeVisible(); }); + + it("hides the source tabs when Registry is unavailable in the deployment", () => { + // Given: Local (OSS) or Registry-flag-off deployments deny discovery. + render(); + + // Then: only the built-in providers are offered, without a Registry tab. + expect(screen.queryByRole("tablist")).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: "Registry" }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /Amazon Web Services/ }), + ).toBeVisible(); + expect( + screen.queryByText("No Registry providers available."), + ).not.toBeInTheDocument(); + expect( + screen.getByRole("textbox", { name: "Search providers" }), + ).toBeVisible(); + }); + + it("falls back to all providers when Registry access is revoked on the Registry tab", async () => { + // Given + const user = userEvent.setup(); + const { rerender } = render(); + await user.click(screen.getByRole("tab", { name: "Registry" })); + expect( + screen.queryByRole("option", { name: /Amazon Web Services/ }), + ).not.toBeInTheDocument(); + + // When: a discovery refresh reports the deployment no longer offers Registry. + rerender(); + + // Then + expect(screen.queryByRole("tablist")).not.toBeInTheDocument(); + expect( + screen.getByRole("option", { name: /Amazon Web Services/ }), + ).toBeVisible(); + + // When / Then: access restored keeps the previous Registry selection. + rerender(); + expect(screen.getByRole("tab", { name: "Registry" })).toHaveAttribute( + "aria-selected", + "true", + ); + expect( + screen.getByRole("option", { name: /Acme Cloud Registry/ }), + ).toBeVisible(); + }); }); diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index 0da4bf2d83..2d589b91bf 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -33,6 +33,12 @@ type ProviderTab = (typeof PROVIDER_TAB)[keyof typeof PROVIDER_TAB]; interface RadioGroupProviderProps { control: Control; + /** + * Whether this deployment offers Registry providers (Cloud or Private Cloud + * with the Registry flag on and a user allowed to manage providers). Off in + * Local (OSS) and flag-off deployments, where the source tabs are hidden. + */ + registryAvailable?: boolean; registryOptions?: RegistryProviderOption[]; isInvalid: boolean; errorMessage?: string; @@ -42,10 +48,14 @@ export const RadioGroupProvider: FC = ({ control, isInvalid, errorMessage, + registryAvailable = false, registryOptions = [], }) => { const [searchTerm, setSearchTerm] = useState(""); - const [activeTab, setActiveTab] = useState(PROVIDER_TAB.ALL); + const [selectedTab, setSelectedTab] = useState(PROVIDER_TAB.ALL); + // Fall back to the full list if Registry access is revoked while the + // Registry tab is selected, so the selector never shows an empty tab. + const activeTab = registryAvailable ? selectedTab : PROVIDER_TAB.ALL; const options = [ ...PROVIDERS.map((provider) => ({ @@ -82,12 +92,14 @@ export const RadioGroupProvider: FC = ({ setActiveTab(value as ProviderTab)} + onValueChange={(value) => setSelectedTab(value as ProviderTab)} > - - All providers - Registry - + {registryAvailable && ( + + All providers + Registry + + )}
{ screen.getByRole("option", { name: /Amazon Web Services/ }), ).toBeVisible(); + // Then: an unanswered discovery cannot vouch for Registry availability. + expect( + screen.queryByRole("tab", { name: "Registry" }), + ).not.toBeInTheDocument(); + // When - await user.click(screen.getByRole("tab", { name: "Registry" })); - expect(screen.getByText("No Registry providers available.")).toBeVisible(); await user.click( screen.getByRole("button", { name: "Retry Registry providers" }), ); @@ -223,8 +226,25 @@ describe("provider wizard account creation", () => { expect( await screen.findByRole("option", { name: "Acme Cloud Registry" }), ).toBeVisible(); + expect(screen.getByRole("tab", { name: "Registry" })).toBeVisible(); expect( screen.queryByText("Registry providers could not be loaded"), ).not.toBeInTheDocument(); }); + + it("keeps the Registry tab with a retry when eligible discovery fails", async () => { + // Given: Cloud with Registry enabled, but the catalog read failed. + getInstalledRegistryProviderOptions.mockResolvedValueOnce({ + status: "error", + }); + const user = userEvent.setup(); + render(); + await screen.findByText("Registry providers could not be loaded"); + + // When + await user.click(screen.getByRole("tab", { name: "Registry" })); + + // Then + expect(screen.getByText("No Registry providers available.")).toBeVisible(); + }); }); 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 7e74064f8e..471034df33 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.test.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.test.tsx @@ -82,3 +82,51 @@ describe("provider account aliases", () => { }); }); }); + +describe("Registry provider source tabs", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("hides the Registry tab when discovery denies access (Local or flag off)", async () => { + // Given + getInstalledRegistryProviderOptions.mockResolvedValue({ + status: "access_denied", + }); + + // When + render(); + await waitFor(() => + expect(getInstalledRegistryProviderOptions).toHaveBeenCalled(), + ); + + // Then + expect( + screen.getByRole("option", { name: /Amazon Web Services/ }), + ).toBeVisible(); + expect( + screen.queryByRole("tab", { name: "Registry" }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole("tab", { name: "All providers" }), + ).not.toBeInTheDocument(); + }); + + it("shows the Registry tab once discovery confirms the deployment offers it", async () => { + // Given: Cloud or Private Cloud with Registry enabled and no artifacts yet. + getInstalledRegistryProviderOptions.mockResolvedValue({ + status: "ready", + options: [], + }); + + // When + render(); + + // Then + expect(await screen.findByRole("tab", { name: "Registry" })).toBeVisible(); + expect(screen.getByRole("tab", { name: "All providers" })).toHaveAttribute( + "aria-selected", + "true", + ); + }); +}); diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx index 2ccd6531ff..aeb9d413ce 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.tsx @@ -218,6 +218,10 @@ export const ConnectAccountForm = ({ const [registryOptions, setRegistryOptions] = useState< RegistryProviderOption[] >([]); + // Only Cloud and Private Cloud deployments with the Registry flag on answer + // discovery with "ready" or "error"; Local (OSS) and flag-off deployments + // are denied and never show the Registry tab. + const [registryAvailable, setRegistryAvailable] = useState(false); const [registryError, setRegistryError] = useState(false); const [providerError, setProviderError] = useState(null); const [discoveryAttempt, setDiscoveryAttempt] = useState(0); @@ -231,10 +235,12 @@ export const ConnectAccountForm = ({ const result = await getInstalledRegistryProviderOptions(); if (!active) return; setRegistryOptions(result.status === "ready" ? result.options : []); + setRegistryAvailable(result.status !== "access_denied"); setRegistryError(result.status === "error"); } catch { if (active) { setRegistryOptions([]); + setRegistryAvailable(false); setRegistryError(true); } } @@ -467,6 +473,7 @@ export const ConnectAccountForm = ({ )} { + await this.page.goto("/providers"); + await this.dismissWelcomeDialog(); + await this.page.getByRole("button", { name: /Add (a )?Provider/i }).click(); + await expect( + this.page.getByRole("option", { + name: "Amazon Web Services", + exact: true, + }), + ).toBeVisible(); + await expect( + this.page.getByRole("tab", { name: "Registry", exact: true }), + ).toBeHidden(); + await expect( + this.page.getByRole("tab", { name: "All providers", exact: true }), + ).toBeHidden(); + } + async verifyOnboarding(): Promise { await expect(this.connectButton).toBeVisible(); await expect( diff --git a/ui/tests/registry/registry.spec.ts b/ui/tests/registry/registry.spec.ts index 99100909b5..a99617ba9b 100644 --- a/ui/tests/registry/registry.spec.ts +++ b/ui/tests/registry/registry.spec.ts @@ -94,6 +94,7 @@ test.describe.serial("Registry", () => { await registryPage.verifyRegistryNavigationHidden(); await registryPage.goto(); await registryPage.verifyDirectRouteDenied(); + await registryPage.verifyProviderSelectorWithoutRegistry(); }, );