feat(ui): highlight imported providers (#12447)

This commit is contained in:
Adrián Peña
2026-08-17 08:53:14 +02:00
committed by GitHub
parent 0d3ce45374
commit 758b696ca5
4 changed files with 240 additions and 19 deletions
@@ -0,0 +1 @@
Prowler Cloud indicator for providers created via Import Findings alongside every connection status
@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
PROVIDERS_ROW_TYPE,
@@ -10,9 +11,18 @@ import {
import { getColumnProviders } from "./column-providers";
vi.mock("@/components/shadcn", () => ({
Badge: ({ children }: { children: ReactNode }) => <span>{children}</span>,
}));
vi.mock("@/components/shadcn", async () => {
const tooltip = await vi.importActual<
typeof import("@/components/shadcn/tooltip")
>("@/components/shadcn/tooltip");
return {
Badge: ({ children, ...props }: { children: ReactNode }) => (
<span {...props}>{children}</span>
),
...tooltip,
};
});
vi.mock("@/components/shadcn/checkbox/checkbox", () => ({
Checkbox: () => null,
@@ -64,6 +74,7 @@ const providerRow: ProvidersProviderRow = {
attributes: {
provider: "aws",
is_dynamic: false,
is_imported: false,
uid: "123456789012",
alias: "Production",
status: "completed",
@@ -115,6 +126,33 @@ function renderLastScanCell(row: ProvidersTableRow) {
render(<>{element as ReactNode}</>);
}
function renderStatusCell(row: ProvidersTableRow) {
const statusColumn = getColumnProviders(
{},
[],
[],
[],
vi.fn(),
vi.fn(),
vi.fn(),
).find((column) => column.id === "status");
const cell = statusColumn?.cell;
if (typeof cell !== "function") {
throw new Error("Status column cell renderer not found");
}
const element = cell({
row: { original: row },
} as unknown as Parameters<typeof cell>[0]);
render(<>{element as ReactNode}</>);
}
afterEach(() => {
vi.unstubAllEnvs();
});
describe("getColumnProviders", () => {
it("falls back to connection last_checked_at when lastScanAt is undefined", () => {
renderLastScanCell({ ...providerRow, lastScanAt: undefined });
@@ -129,4 +167,133 @@ describe("getColumnProviders", () => {
expect(screen.getByText("Never")).toBeVisible();
expect(screen.queryByText("2026-01-01T00:00:00Z")).not.toBeInTheDocument();
});
it("shows Not connected with the imported tooltip in Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
const importedProvider = {
...providerRow,
attributes: {
...providerRow.attributes,
is_imported: true,
connection: {
connected: null,
last_checked_at: null,
},
},
relationships: {
...providerRow.relationships,
secret: { data: null },
},
} satisfies ProvidersProviderRow;
// When
renderStatusCell(importedProvider);
// Then
expect(screen.getByText("Not connected")).toBeVisible();
const indicator = screen.getByLabelText("Imported provider");
expect(indicator).not.toHaveTextContent("Imported");
expect(indicator).toHaveAttribute("tabindex", "0");
expect(indicator.querySelector(".lucide-terminal")).toBeInTheDocument();
expect(indicator.parentElement).toHaveClass("items-stretch");
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
expect(screen.queryByText("Disconnected")).not.toBeInTheDocument();
await user.hover(indicator);
expect(await screen.findByRole("tooltip")).toHaveTextContent(
/^This provider has findings imported with the Prowler CLI$/,
);
});
it("shows the imported tooltip beside Connected in Cloud", async () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const user = userEvent.setup();
const connectedImportedProvider = {
...providerRow,
attributes: {
...providerRow.attributes,
is_imported: true,
connection: {
connected: true,
last_checked_at: "2026-01-01T00:00:00Z",
},
},
} satisfies ProvidersProviderRow;
// When
renderStatusCell(connectedImportedProvider);
// Then
expect(screen.getByText("Connected")).toBeVisible();
const indicator = screen.getByLabelText("Imported provider");
expect(indicator).toBeVisible();
expect(indicator.querySelector(".lucide-terminal")).toBeInTheDocument();
expect(indicator.parentElement).toHaveClass("items-stretch");
expect(indicator).not.toHaveTextContent("Imported");
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
await user.hover(indicator);
expect(await screen.findByRole("tooltip")).toHaveTextContent(
/^This provider has findings imported with the Prowler CLI$/,
);
});
it("keeps Connection failed for imported providers in Cloud", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const importedProviderWithCredentials = {
...providerRow,
attributes: {
...providerRow.attributes,
is_imported: true,
connection: {
connected: false,
last_checked_at: null,
},
},
} satisfies ProvidersProviderRow;
// When
renderStatusCell(importedProviderWithCredentials);
// Then
expect(screen.getByText("Connection failed")).toBeVisible();
expect(screen.queryByText("Not connected")).not.toBeInTheDocument();
const indicator = screen.getByLabelText("Imported provider");
expect(indicator).not.toHaveTextContent("Imported");
});
it("does not expose imported provenance in OSS", () => {
// Given
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const importedProvider = {
...providerRow,
attributes: {
...providerRow.attributes,
is_imported: true,
connection: {
connected: null,
last_checked_at: null,
},
},
relationships: {
...providerRow.relationships,
secret: { data: null },
},
} satisfies ProvidersProviderRow;
// When
renderStatusCell(importedProvider);
// Then
expect(screen.getByText("Not connected")).toBeVisible();
expect(
screen.queryByLabelText("Imported provider"),
).not.toBeInTheDocument();
});
});
@@ -1,13 +1,18 @@
"use client";
import { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table";
import { Building2, FolderTree } from "lucide-react";
import { Building2, FolderTree, Terminal } from "lucide-react";
import type {
OrgWizardInitialData,
ProviderWizardInitialData,
} from "@/components/providers/wizard/types";
import { Badge } from "@/components/shadcn";
import {
Badge,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn";
import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
import { DateWithTime, EntityInfo } from "@/components/shadcn/entities";
@@ -15,6 +20,7 @@ import { DataTableColumnHeader } from "@/components/shadcn/table";
import { DataTableExpandAllToggle } from "@/components/shadcn/table/data-table-expand-all-toggle";
import { DataTableExpandableCell } from "@/components/shadcn/table/data-table-expandable-cell";
import { getNodeLabel } from "@/lib/organizations";
import { isCloud } from "@/lib/shared/env";
import {
isProvidersOrganizationRow,
PROVIDERS_GROUP_KIND,
@@ -51,28 +57,70 @@ const OrganizationIcon = ({ groupKind }: { groupKind: ProvidersGroupKind }) => {
);
};
const ProviderStatusCell = ({ connected }: { connected: boolean | null }) => {
if (connected === true) {
interface ProviderStatusCellProps {
connected: boolean | null;
isImported: boolean;
}
const IMPORTED_PROVIDER_TOOLTIP =
"This provider has findings imported with the Prowler CLI";
const ImportedIndicator = () => {
const indicator = (
<Badge
variant="info"
className="text-sm"
aria-label="Imported provider"
tabIndex={0}
>
<Terminal aria-hidden />
</Badge>
);
return (
<Tooltip>
<TooltipTrigger asChild>{indicator}</TooltipTrigger>
<TooltipContent>{IMPORTED_PROVIDER_TOOLTIP}</TooltipContent>
</Tooltip>
);
};
const ProviderStatusCell = ({
connected,
isImported,
}: ProviderStatusCellProps) => {
let statusBadge;
if (connected === true) {
statusBadge = (
<Badge variant="success" className="text-sm">
Connected
</Badge>
);
}
if (connected === false) {
return (
} else if (connected === false) {
statusBadge = (
<Badge variant="error" className="text-sm">
Connection failed
</Badge>
);
}
return (
} else {
statusBadge = (
<Badge variant="tag" className="text-text-neutral-secondary text-sm">
Not connected
</Badge>
);
}
if (isImported) {
return (
<div className="flex items-stretch gap-2">
{statusBadge}
<ImportedIndicator />
</div>
);
}
return statusBadge;
};
function getSelectionLabel(row: Row<ProvidersTableRow>): string | undefined {
@@ -299,6 +347,9 @@ export function getColumnProviders(
return (
<ProviderStatusCell
connected={row.original.attributes.connection.connected}
isImported={
isCloud() && Boolean(row.original.attributes.is_imported)
}
/>
);
},
+4 -2
View File
@@ -80,13 +80,15 @@ export interface ProviderProps {
attributes: {
provider: ProviderType;
is_dynamic: boolean;
/** Import Findings provenance returned only by the Prowler Cloud API. */
is_imported?: boolean;
uid: string;
alias: string;
status: "completed" | "pending" | "cancelled";
resources: number;
connection: {
connected: boolean;
last_checked_at: string;
connected: boolean | null;
last_checked_at: string | null;
};
scanner_args: {
only_logs: boolean;