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 { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PROVIDERS_ROW_TYPE,
|
PROVIDERS_ROW_TYPE,
|
||||||
@@ -10,9 +11,18 @@ import {
|
|||||||
|
|
||||||
import { getColumnProviders } from "./column-providers";
|
import { getColumnProviders } from "./column-providers";
|
||||||
|
|
||||||
vi.mock("@/components/shadcn", () => ({
|
vi.mock("@/components/shadcn", async () => {
|
||||||
Badge: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
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", () => ({
|
vi.mock("@/components/shadcn/checkbox/checkbox", () => ({
|
||||||
Checkbox: () => null,
|
Checkbox: () => null,
|
||||||
@@ -64,6 +74,7 @@ const providerRow: ProvidersProviderRow = {
|
|||||||
attributes: {
|
attributes: {
|
||||||
provider: "aws",
|
provider: "aws",
|
||||||
is_dynamic: false,
|
is_dynamic: false,
|
||||||
|
is_imported: false,
|
||||||
uid: "123456789012",
|
uid: "123456789012",
|
||||||
alias: "Production",
|
alias: "Production",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
@@ -115,6 +126,33 @@ function renderLastScanCell(row: ProvidersTableRow) {
|
|||||||
render(<>{element as ReactNode}</>);
|
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", () => {
|
describe("getColumnProviders", () => {
|
||||||
it("falls back to connection last_checked_at when lastScanAt is undefined", () => {
|
it("falls back to connection last_checked_at when lastScanAt is undefined", () => {
|
||||||
renderLastScanCell({ ...providerRow, lastScanAt: undefined });
|
renderLastScanCell({ ...providerRow, lastScanAt: undefined });
|
||||||
@@ -129,4 +167,133 @@ describe("getColumnProviders", () => {
|
|||||||
expect(screen.getByText("Never")).toBeVisible();
|
expect(screen.getByText("Never")).toBeVisible();
|
||||||
expect(screen.queryByText("2026-01-01T00:00:00Z")).not.toBeInTheDocument();
|
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";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table";
|
import { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table";
|
||||||
import { Building2, FolderTree } from "lucide-react";
|
import { Building2, FolderTree, Terminal } from "lucide-react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OrgWizardInitialData,
|
OrgWizardInitialData,
|
||||||
ProviderWizardInitialData,
|
ProviderWizardInitialData,
|
||||||
} from "@/components/providers/wizard/types";
|
} 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 { Checkbox } from "@/components/shadcn/checkbox/checkbox";
|
||||||
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
|
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
|
||||||
import { DateWithTime, EntityInfo } from "@/components/shadcn/entities";
|
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 { DataTableExpandAllToggle } from "@/components/shadcn/table/data-table-expand-all-toggle";
|
||||||
import { DataTableExpandableCell } from "@/components/shadcn/table/data-table-expandable-cell";
|
import { DataTableExpandableCell } from "@/components/shadcn/table/data-table-expandable-cell";
|
||||||
import { getNodeLabel } from "@/lib/organizations";
|
import { getNodeLabel } from "@/lib/organizations";
|
||||||
|
import { isCloud } from "@/lib/shared/env";
|
||||||
import {
|
import {
|
||||||
isProvidersOrganizationRow,
|
isProvidersOrganizationRow,
|
||||||
PROVIDERS_GROUP_KIND,
|
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) {
|
if (connected === true) {
|
||||||
return (
|
statusBadge = (
|
||||||
<Badge variant="success" className="text-sm">
|
<Badge variant="success" className="text-sm">
|
||||||
Connected
|
Connected
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
} else if (connected === false) {
|
||||||
|
statusBadge = (
|
||||||
if (connected === false) {
|
|
||||||
return (
|
|
||||||
<Badge variant="error" className="text-sm">
|
<Badge variant="error" className="text-sm">
|
||||||
Connection failed
|
Connection failed
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
statusBadge = (
|
||||||
|
<Badge variant="tag" className="text-text-neutral-secondary text-sm">
|
||||||
|
Not connected
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (isImported) {
|
||||||
<Badge variant="tag" className="text-text-neutral-secondary text-sm">
|
return (
|
||||||
Not connected
|
<div className="flex items-stretch gap-2">
|
||||||
</Badge>
|
{statusBadge}
|
||||||
);
|
<ImportedIndicator />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusBadge;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSelectionLabel(row: Row<ProvidersTableRow>): string | undefined {
|
function getSelectionLabel(row: Row<ProvidersTableRow>): string | undefined {
|
||||||
@@ -299,6 +347,9 @@ export function getColumnProviders(
|
|||||||
return (
|
return (
|
||||||
<ProviderStatusCell
|
<ProviderStatusCell
|
||||||
connected={row.original.attributes.connection.connected}
|
connected={row.original.attributes.connection.connected}
|
||||||
|
isImported={
|
||||||
|
isCloud() && Boolean(row.original.attributes.is_imported)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,13 +80,15 @@ export interface ProviderProps {
|
|||||||
attributes: {
|
attributes: {
|
||||||
provider: ProviderType;
|
provider: ProviderType;
|
||||||
is_dynamic: boolean;
|
is_dynamic: boolean;
|
||||||
|
/** Import Findings provenance returned only by the Prowler Cloud API. */
|
||||||
|
is_imported?: boolean;
|
||||||
uid: string;
|
uid: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
status: "completed" | "pending" | "cancelled";
|
status: "completed" | "pending" | "cancelled";
|
||||||
resources: number;
|
resources: number;
|
||||||
connection: {
|
connection: {
|
||||||
connected: boolean;
|
connected: boolean | null;
|
||||||
last_checked_at: string;
|
last_checked_at: string | null;
|
||||||
};
|
};
|
||||||
scanner_args: {
|
scanner_args: {
|
||||||
only_logs: boolean;
|
only_logs: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user