mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
fix(ui): improve attack paths scan table UX and fix info banner variant (#10704)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com> Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
co-authored by
Pepe Fagoaga
alejandrobailo
parent
9a0c73256e
commit
9e31160887
@@ -84,6 +84,7 @@ continue.json
|
||||
.continuerc.json
|
||||
|
||||
# AI Coding Assistants - OpenCode
|
||||
.opencode/
|
||||
opencode.json
|
||||
|
||||
# AI Coding Assistants - GitHub Copilot
|
||||
|
||||
+1
-3
@@ -12,11 +12,9 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🔄 Changed
|
||||
|
||||
- Attack Paths scan selection: contextual button labels based on graph availability, tooltips on disabled actions, green dot indicator for selectable scans, and a warning banner when viewing data from a previous scan cycle [(#10685)](https://github.com/prowler-cloud/prowler/pull/10685)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Remove legacy finding detail sheet, row-details wrapper, and resource detail panel; unify findings and resources around new side drawers [(#10692)](https://github.com/prowler-cloud/prowler/pull/10692)
|
||||
- Attack Paths "View Finding" now opens the finding drawer inline over the graph instead of navigating to `/findings` in a new tab, preserving graph zoom, selection, and filter state
|
||||
- Attack Paths scan table: replace action buttons with radio buttons, add dedicated Graph column, use info-colored In Progress badge, remove redundant Progress column, and fix info banner variant [(#10704)](https://github.com/prowler-cloud/prowler/pull/10704)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
|
||||
+47
-30
@@ -169,14 +169,14 @@ describe("ScanListTable", () => {
|
||||
expect(screen.getByText("12 Total Entries")).toBeInTheDocument();
|
||||
expect(screen.getByText("Page 1 of 3")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getAllByRole("button", { name: "Select scan" })[0]);
|
||||
await user.click(screen.getAllByRole("radio", { name: "Select scan" })[0]);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("enables the select button for a failed scan when graph data is ready", async () => {
|
||||
it("enables the radio button for a failed scan when graph data is ready", async () => {
|
||||
const user = userEvent.setup();
|
||||
const failedScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
@@ -189,18 +189,18 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[failedScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
const radio = screen.getByRole("radio", { name: "Select scan" });
|
||||
expect(radio).toBeEnabled();
|
||||
expect(radio).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(button);
|
||||
await user.click(radio);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("disables the select button for a failed scan when graph data is not ready", () => {
|
||||
it("disables the radio button for a failed scan when graph data is not ready", () => {
|
||||
const failedScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -212,12 +212,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[failedScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Failed");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows 'Scheduled' label for a scheduled scan without graph data", () => {
|
||||
it("shows a disabled radio button for a scheduled scan without graph data", () => {
|
||||
const scheduledScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -232,12 +231,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[scheduledScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Scheduled");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows 'Running...' label for an executing scan without graph data", () => {
|
||||
it("shows a disabled radio button for an executing scan without graph data", () => {
|
||||
const executingScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -252,12 +250,11 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[executingScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Running...");
|
||||
const radio = screen.getByRole("radio", { name: "Scan not available" });
|
||||
expect(radio).toBeDisabled();
|
||||
});
|
||||
|
||||
it("enables Select for a scheduled scan when graph data is ready from a previous cycle", async () => {
|
||||
it("enables the radio button for a scheduled scan when graph data is ready from a previous cycle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const scheduledWithGraph: AttackPathScan = {
|
||||
...createScan(1),
|
||||
@@ -271,26 +268,26 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[scheduledWithGraph]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
const radio = screen.getByRole("radio", { name: "Select scan" });
|
||||
expect(radio).toBeEnabled();
|
||||
expect(radio).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
await user.click(button);
|
||||
await user.click(radio);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a green dot next to the account name when graph data is ready", () => {
|
||||
it("exposes an accessible label in the Graph column when graph data is ready", () => {
|
||||
render(<ScanListTable scans={[createScan(1)]} />);
|
||||
|
||||
const dot = screen.getByLabelText("Graph data available");
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(dot).toHaveClass("bg-bg-pass-primary");
|
||||
expect(screen.getByLabelText("Graph available")).toHaveClass(
|
||||
"text-text-success-primary",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not show a green dot when graph data is not ready", () => {
|
||||
it("exposes an accessible label in the Graph column when graph data is not ready", () => {
|
||||
const noGraphScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
@@ -301,8 +298,28 @@ describe("ScanListTable", () => {
|
||||
|
||||
render(<ScanListTable scans={[noGraphScan]} />);
|
||||
|
||||
expect(screen.getByLabelText("Graph not available")).toHaveClass(
|
||||
"text-text-neutral-secondary",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders a tooltip explaining a completed scan without graph data", () => {
|
||||
const completedNoGraph: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "completed",
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[completedNoGraph]} />);
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText("Graph data available"),
|
||||
).not.toBeInTheDocument();
|
||||
screen.getByRole("radio", { name: "Scan not available" }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByText("This scan completed without producing graph data."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+104
-127
@@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Check, Minus } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/components/shadcn/radio-group/radio-group";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -38,44 +42,6 @@ const formatNullableDuration = (duration: number | null) => {
|
||||
return formatDuration(duration);
|
||||
};
|
||||
|
||||
const isSelectDisabled = (
|
||||
scan: AttackPathScan,
|
||||
selectedScanId: string | null,
|
||||
) => {
|
||||
return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
|
||||
};
|
||||
|
||||
const getSelectButtonLabel = (
|
||||
scan: AttackPathScan,
|
||||
selectedScanId: string | null,
|
||||
) => {
|
||||
if (selectedScanId === scan.id) {
|
||||
return "Selected";
|
||||
}
|
||||
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return "Select";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Scheduled";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.AVAILABLE) {
|
||||
return "Queued";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
return "Running...";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
return "Failed";
|
||||
}
|
||||
|
||||
return "Select";
|
||||
};
|
||||
|
||||
const getDisabledTooltip = (scan: AttackPathScan): string | null => {
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return null;
|
||||
@@ -97,7 +63,11 @@ const getDisabledTooltip = (scan: AttackPathScan): string | null => {
|
||||
return "This scan failed. No graph data is available.";
|
||||
}
|
||||
|
||||
return null;
|
||||
if (scan.attributes.state === SCAN_STATES.COMPLETED) {
|
||||
return "This scan completed without producing graph data.";
|
||||
}
|
||||
|
||||
return "Graph data is not available for this scan.";
|
||||
};
|
||||
|
||||
const getSelectedRowSelection = (
|
||||
@@ -129,37 +99,65 @@ const buildMetadata = (
|
||||
|
||||
const getColumns = ({
|
||||
selectedScanId,
|
||||
onSelectScan,
|
||||
}: {
|
||||
selectedScanId: string | null;
|
||||
onSelectScan: (scanId: string) => void;
|
||||
}): ColumnDef<AttackPathScan>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: () => <span className="text-sm font-medium">Select</span>,
|
||||
cell: ({ row }) => {
|
||||
const isSelected = selectedScanId === row.original.id;
|
||||
const canSelect = row.original.attributes.graph_data_ready;
|
||||
const tooltip = getDisabledTooltip(row.original);
|
||||
|
||||
const radio = (
|
||||
<RadioGroupItem
|
||||
value={row.original.id}
|
||||
checked={isSelected}
|
||||
disabled={!canSelect}
|
||||
className={cn(
|
||||
"size-5",
|
||||
canSelect &&
|
||||
!isSelected &&
|
||||
"border-text-neutral-secondary cursor-pointer",
|
||||
!canSelect && "disabled:opacity-70",
|
||||
)}
|
||||
aria-label={
|
||||
isSelected
|
||||
? "Selected scan"
|
||||
: canSelect
|
||||
? "Select scan"
|
||||
: "Scan not available"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!canSelect && !isSelected && tooltip) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span tabIndex={0}>{radio}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return radio;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "provider",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Account" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block size-2 shrink-0 rounded-full",
|
||||
row.original.attributes.graph_data_ready
|
||||
? "bg-bg-pass-primary"
|
||||
: "bg-transparent",
|
||||
)}
|
||||
aria-label={
|
||||
row.original.attributes.graph_data_ready
|
||||
? "Graph data available"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
</div>
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -182,22 +180,32 @@ const getColumns = ({
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ScanStatusBadge
|
||||
status={row.original.attributes.state}
|
||||
progress={row.original.attributes.progress}
|
||||
graphDataReady={row.original.attributes.graph_data_ready}
|
||||
/>
|
||||
<div className="flex">
|
||||
<ScanStatusBadge
|
||||
status={row.original.attributes.state}
|
||||
progress={row.original.attributes.progress}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "progress",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Progress" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm">{row.original.attributes.progress}%</span>
|
||||
),
|
||||
accessorKey: "graph_data_ready",
|
||||
header: () => <span className="text-sm font-medium">Graph</span>,
|
||||
cell: ({ row }) =>
|
||||
row.original.attributes.graph_data_ready ? (
|
||||
<Check
|
||||
size={16}
|
||||
aria-label="Graph available"
|
||||
className="text-text-success-primary"
|
||||
/>
|
||||
) : (
|
||||
<Minus
|
||||
size={16}
|
||||
aria-label="Graph not available"
|
||||
className="text-text-neutral-secondary"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
@@ -212,45 +220,6 @@ const getColumns = ({
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => {
|
||||
const isDisabled = isSelectDisabled(row.original, selectedScanId);
|
||||
const tooltip = getDisabledTooltip(row.original);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isDisabled && tooltip) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="w-full max-w-24" tabIndex={0}>
|
||||
{button}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex justify-end">{button}</div>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -300,19 +269,27 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={getColumns({
|
||||
selectedScanId,
|
||||
onSelectScan: handleSelectScan,
|
||||
})}
|
||||
data={paginatedScans}
|
||||
metadata={buildMetadata(scans.length, currentPage, totalPages)}
|
||||
controlledPage={currentPage}
|
||||
controlledPageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
enableRowSelection
|
||||
rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
|
||||
/>
|
||||
<RadioGroup
|
||||
value={selectedScanId ?? ""}
|
||||
onValueChange={handleSelectScan}
|
||||
className="gap-0"
|
||||
>
|
||||
<DataTable
|
||||
columns={getColumns({ selectedScanId })}
|
||||
data={paginatedScans}
|
||||
metadata={buildMetadata(scans.length, currentPage, totalPages)}
|
||||
controlledPage={currentPage}
|
||||
controlledPageSize={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
onRowClick={(row) => {
|
||||
if (row.original.attributes.graph_data_ready) {
|
||||
handleSelectScan(row.original.id);
|
||||
}
|
||||
}}
|
||||
enableRowSelection
|
||||
rowSelection={getSelectedRowSelection(paginatedScans, selectedScanId)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-50
@@ -3,97 +3,55 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
const BADGE_CONFIG: Record<
|
||||
ScanState,
|
||||
{ className: string; label: string; showGraphDot: boolean }
|
||||
> = {
|
||||
const BADGE_CONFIG: Record<ScanState, { className: string; label: string }> = {
|
||||
[SCAN_STATES.SCHEDULED]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Scheduled",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.AVAILABLE]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Queued",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.EXECUTING]: {
|
||||
className: "bg-bg-warning-secondary text-text-neutral-primary",
|
||||
className: "bg-bg-info-secondary text-text-info",
|
||||
label: "In Progress",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.COMPLETED]: {
|
||||
className: "bg-bg-pass-secondary text-text-success-primary",
|
||||
label: "Completed",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.FAILED]: {
|
||||
className: "bg-bg-fail-secondary text-text-error-primary",
|
||||
label: "Failed",
|
||||
showGraphDot: true,
|
||||
},
|
||||
};
|
||||
|
||||
interface ScanStatusBadgeProps {
|
||||
status: ScanState;
|
||||
progress?: number;
|
||||
graphDataReady?: boolean;
|
||||
}
|
||||
|
||||
export const ScanStatusBadge = ({
|
||||
status,
|
||||
progress = 0,
|
||||
graphDataReady = false,
|
||||
}: ScanStatusBadgeProps) => {
|
||||
const config = BADGE_CONFIG[status];
|
||||
|
||||
const graphDot = graphDataReady && config.showGraphDot && (
|
||||
<span className="bg-bg-pass-primary inline-block size-2 rounded-full" />
|
||||
);
|
||||
|
||||
const tooltipText = graphDataReady
|
||||
? "Graph available"
|
||||
: status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
|
||||
? "Graph not available"
|
||||
: "Graph not available yet";
|
||||
|
||||
const icon =
|
||||
status === SCAN_STATES.EXECUTING ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className={
|
||||
graphDataReady
|
||||
? "text-text-success-primary animate-spin"
|
||||
: "animate-spin"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
graphDot
|
||||
);
|
||||
|
||||
const label =
|
||||
status === SCAN_STATES.EXECUTING
|
||||
? `${config.label} (${progress}%)`
|
||||
: config.label;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className={cn(config.className, "gap-2")}>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Badge className={cn(config.className, "gap-2")}>
|
||||
{status === SCAN_STATES.EXECUTING && (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Info, Maximize2, TriangleAlert, X } from "lucide-react";
|
||||
import { ArrowLeft, Info, Maximize2, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
@@ -120,8 +120,8 @@ export default function AttackPathsPage() {
|
||||
// Check if there's an executing scan for auto-refresh
|
||||
const hasExecutingScan = scans.some(
|
||||
(scan) =>
|
||||
scan.attributes.state === "executing" ||
|
||||
scan.attributes.state === "scheduled",
|
||||
scan.attributes.state === SCAN_STATES.EXECUTING ||
|
||||
scan.attributes.state === SCAN_STATES.SCHEDULED,
|
||||
);
|
||||
|
||||
// Detect if the selected scan is showing data from a previous cycle
|
||||
@@ -358,11 +358,11 @@ export default function AttackPathsPage() {
|
||||
<h2 className="dark:text-prowler-theme-pale/90 text-xl font-semibold">
|
||||
Attack Paths
|
||||
</h2>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-2 text-sm">
|
||||
<p className="text-text-neutral-secondary mt-2 text-sm">
|
||||
Select a scan, build a query, and visualize Attack Paths in your
|
||||
infrastructure.
|
||||
</p>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-1 text-xs">
|
||||
<p className="text-text-neutral-secondary mt-1 text-xs">
|
||||
Scans can be selected when data is available. A new scan does not
|
||||
interrupt access to existing data.
|
||||
</p>
|
||||
@@ -394,11 +394,8 @@ export default function AttackPathsPage() {
|
||||
|
||||
{/* Banner: viewing data from a previous scan cycle */}
|
||||
{isViewingPreviousCycleData && (
|
||||
<Alert
|
||||
variant="default"
|
||||
className="border-border-warning-secondary bg-bg-warning-secondary"
|
||||
>
|
||||
<TriangleAlert className="text-text-warning-primary size-4" />
|
||||
<Alert variant="info">
|
||||
<Info className="size-4" />
|
||||
<AlertTitle>Viewing data from a previous scan</AlertTitle>
|
||||
<AlertDescription>
|
||||
This scan is currently{" "}
|
||||
@@ -605,7 +602,7 @@ export default function AttackPathsPage() {
|
||||
<X size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mb-4 text-xs">
|
||||
<p className="text-text-neutral-secondary mb-4 text-xs">
|
||||
{graphState.selectedNode?.labels.some(
|
||||
(label) =>
|
||||
label
|
||||
@@ -628,7 +625,7 @@ export default function AttackPathsPage() {
|
||||
<h4 className="mb-2 text-xs font-semibold">
|
||||
Type
|
||||
</h4>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary text-xs">
|
||||
<p className="text-text-neutral-secondary text-xs">
|
||||
{graphState.selectedNode?.labels
|
||||
.map(formatNodeLabel)
|
||||
.join(", ")}
|
||||
@@ -678,7 +675,7 @@ export default function AttackPathsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Node Details</h3>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-1 text-sm">
|
||||
<p className="text-text-neutral-secondary mt-1 text-sm">
|
||||
{String(
|
||||
graphState.selectedNode.labels.some((label) =>
|
||||
label.toLowerCase().includes("finding"),
|
||||
|
||||
Reference in New Issue
Block a user