fix(ui): hide the Registry tab in Add Provider when Registry is unavailable (#12837)

This commit is contained in:
Alejandro Bailo
2026-09-17 17:22:21 +02:00
committed by GitHub
parent bbb297aee7
commit 61d13f078c
7 changed files with 166 additions and 8 deletions
@@ -9,8 +9,10 @@ import type { AddProviderFormValues } from "@/types/formSchemas";
import { RadioGroupProvider } from "./radio-group-provider"; import { RadioGroupProvider } from "./radio-group-provider";
function Selector({ function Selector({
registryAvailable = true,
registryOptions = [{ type: "acme", label: "Acme Cloud" }], registryOptions = [{ type: "acme", label: "Acme Cloud" }],
}: { }: {
registryAvailable?: boolean;
registryOptions?: RegistryProviderOption[]; registryOptions?: RegistryProviderOption[];
}) { }) {
const form = useForm<AddProviderFormValues>(); const form = useForm<AddProviderFormValues>();
@@ -18,6 +20,7 @@ function Selector({
<RadioGroupProvider <RadioGroupProvider
control={form.control} control={form.control}
isInvalid={false} isInvalid={false}
registryAvailable={registryAvailable}
registryOptions={registryOptions} registryOptions={registryOptions}
/> />
); );
@@ -131,4 +134,53 @@ describe("provider selector", () => {
screen.getByRole("option", { name: /Amazon Web Services/ }), screen.getByRole("option", { name: /Amazon Web Services/ }),
).toBeVisible(); ).toBeVisible();
}); });
it("hides the source tabs when Registry is unavailable in the deployment", () => {
// Given: Local (OSS) or Registry-flag-off deployments deny discovery.
render(<Selector registryAvailable={false} registryOptions={[]} />);
// 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(<Selector />);
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(<Selector registryAvailable={false} registryOptions={[]} />);
// 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(<Selector />);
expect(screen.getByRole("tab", { name: "Registry" })).toHaveAttribute(
"aria-selected",
"true",
);
expect(
screen.getByRole("option", { name: /Acme Cloud Registry/ }),
).toBeVisible();
});
}); });
@@ -33,6 +33,12 @@ type ProviderTab = (typeof PROVIDER_TAB)[keyof typeof PROVIDER_TAB];
interface RadioGroupProviderProps { interface RadioGroupProviderProps {
control: Control<AddProviderFormValues>; control: Control<AddProviderFormValues>;
/**
* 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[]; registryOptions?: RegistryProviderOption[];
isInvalid: boolean; isInvalid: boolean;
errorMessage?: string; errorMessage?: string;
@@ -42,10 +48,14 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
control, control,
isInvalid, isInvalid,
errorMessage, errorMessage,
registryAvailable = false,
registryOptions = [], registryOptions = [],
}) => { }) => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [activeTab, setActiveTab] = useState<ProviderTab>(PROVIDER_TAB.ALL); const [selectedTab, setSelectedTab] = useState<ProviderTab>(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 = [ const options = [
...PROVIDERS.map((provider) => ({ ...PROVIDERS.map((provider) => ({
@@ -82,12 +92,14 @@ export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
<Tabs <Tabs
className="flex flex-col px-4" className="flex flex-col px-4"
value={activeTab} value={activeTab}
onValueChange={(value) => setActiveTab(value as ProviderTab)} onValueChange={(value) => setSelectedTab(value as ProviderTab)}
> >
<TabsList aria-label="Provider source"> {registryAvailable && (
<TabsTrigger value={PROVIDER_TAB.ALL}>All providers</TabsTrigger> <TabsList aria-label="Provider source">
<TabsTrigger value={PROVIDER_TAB.REGISTRY}>Registry</TabsTrigger> <TabsTrigger value={PROVIDER_TAB.ALL}>All providers</TabsTrigger>
</TabsList> <TabsTrigger value={PROVIDER_TAB.REGISTRY}>Registry</TabsTrigger>
</TabsList>
)}
<div className="relative z-10 shrink-0 py-4"> <div className="relative z-10 shrink-0 py-4">
<SearchInput <SearchInput
aria-label="Search providers" aria-label="Search providers"
@@ -212,9 +212,12 @@ describe("provider wizard account creation", () => {
screen.getByRole("option", { name: /Amazon Web Services/ }), screen.getByRole("option", { name: /Amazon Web Services/ }),
).toBeVisible(); ).toBeVisible();
// Then: an unanswered discovery cannot vouch for Registry availability.
expect(
screen.queryByRole("tab", { name: "Registry" }),
).not.toBeInTheDocument();
// When // When
await user.click(screen.getByRole("tab", { name: "Registry" }));
expect(screen.getByText("No Registry providers available.")).toBeVisible();
await user.click( await user.click(
screen.getByRole("button", { name: "Retry Registry providers" }), screen.getByRole("button", { name: "Retry Registry providers" }),
); );
@@ -223,8 +226,25 @@ describe("provider wizard account creation", () => {
expect( expect(
await screen.findByRole("option", { name: "Acme Cloud Registry" }), await screen.findByRole("option", { name: "Acme Cloud Registry" }),
).toBeVisible(); ).toBeVisible();
expect(screen.getByRole("tab", { name: "Registry" })).toBeVisible();
expect( expect(
screen.queryByText("Registry providers could not be loaded"), screen.queryByText("Registry providers could not be loaded"),
).not.toBeInTheDocument(); ).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(<ProviderWizardModal open onOpenChange={vi.fn()} />);
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();
});
}); });
@@ -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(<ConnectAccountForm onSuccess={vi.fn()} />);
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(<ConnectAccountForm onSuccess={vi.fn()} />);
// Then
expect(await screen.findByRole("tab", { name: "Registry" })).toBeVisible();
expect(screen.getByRole("tab", { name: "All providers" })).toHaveAttribute(
"aria-selected",
"true",
);
});
});
@@ -218,6 +218,10 @@ export const ConnectAccountForm = ({
const [registryOptions, setRegistryOptions] = useState< const [registryOptions, setRegistryOptions] = useState<
RegistryProviderOption[] 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 [registryError, setRegistryError] = useState(false);
const [providerError, setProviderError] = useState<string | null>(null); const [providerError, setProviderError] = useState<string | null>(null);
const [discoveryAttempt, setDiscoveryAttempt] = useState(0); const [discoveryAttempt, setDiscoveryAttempt] = useState(0);
@@ -231,10 +235,12 @@ export const ConnectAccountForm = ({
const result = await getInstalledRegistryProviderOptions(); const result = await getInstalledRegistryProviderOptions();
if (!active) return; if (!active) return;
setRegistryOptions(result.status === "ready" ? result.options : []); setRegistryOptions(result.status === "ready" ? result.options : []);
setRegistryAvailable(result.status !== "access_denied");
setRegistryError(result.status === "error"); setRegistryError(result.status === "error");
} catch { } catch {
if (active) { if (active) {
setRegistryOptions([]); setRegistryOptions([]);
setRegistryAvailable(false);
setRegistryError(true); setRegistryError(true);
} }
} }
@@ -467,6 +473,7 @@ export const ConnectAccountForm = ({
</Alert> </Alert>
)} )}
<RadioGroupProvider <RadioGroupProvider
registryAvailable={registryAvailable}
registryOptions={registryOptions} registryOptions={registryOptions}
control={form.control} control={form.control}
isInvalid={!!form.formState.errors.providerType} isInvalid={!!form.formState.errors.providerType}
+18
View File
@@ -79,6 +79,24 @@ export class RegistryPage extends BasePage {
await expect(this.registryLink).toBeHidden(); await expect(this.registryLink).toBeHidden();
} }
async verifyProviderSelectorWithoutRegistry(): Promise<void> {
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<void> { async verifyOnboarding(): Promise<void> {
await expect(this.connectButton).toBeVisible(); await expect(this.connectButton).toBeVisible();
await expect( await expect(
+1
View File
@@ -94,6 +94,7 @@ test.describe.serial("Registry", () => {
await registryPage.verifyRegistryNavigationHidden(); await registryPage.verifyRegistryNavigationHidden();
await registryPage.goto(); await registryPage.goto();
await registryPage.verifyDirectRouteDenied(); await registryPage.verifyDirectRouteDenied();
await registryPage.verifyProviderSelectorWithoutRegistry();
}, },
); );