mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
feat(ui): highlight imported providers (#12447)
This commit is contained in:
@@ -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 }) => {
|
||||
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) {
|
||||
return (
|
||||
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>
|
||||
);
|
||||
} else {
|
||||
statusBadge = (
|
||||
<Badge variant="tag" className="text-text-neutral-secondary text-sm">
|
||||
Not connected
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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)
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user