mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
fix(ui): hide the Registry tab in Add Provider when Registry is unavailable (#12837)
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user