mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
Compare commits
8 Commits
62e92e3f85
...
9f4063a6f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4063a6f1 | ||
|
|
d8a5127943 | ||
|
|
895fb80ea5 | ||
|
|
91fe7bc3c4 | ||
|
|
8822663e7f | ||
|
|
83950d53f0 | ||
|
|
dce87293d9 | ||
|
|
df3bd045b4 |
@@ -3,6 +3,66 @@
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
/**
|
||||
* Resolves resource UIDs + check ID into actual finding UUIDs.
|
||||
* Uses /findings/latest with check_id and resource_uid__in filters
|
||||
* to batch-resolve in a single API call.
|
||||
*/
|
||||
export const resolveFindingIds = async ({
|
||||
checkId,
|
||||
resourceUids,
|
||||
}: {
|
||||
checkId: string;
|
||||
resourceUids: string[];
|
||||
}): Promise<string[]> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/findings/latest`);
|
||||
url.searchParams.append("filter[check_id]", checkId);
|
||||
url.searchParams.append("filter[resource_uid__in]", resourceUids.join(","));
|
||||
url.searchParams.append("page[size]", resourceUids.length.toString());
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
const data = await handleApiResponse(response);
|
||||
|
||||
if (!data?.data || !Array.isArray(data.data)) return [];
|
||||
|
||||
return data.data.map((item: { id: string }) => item.id);
|
||||
} catch (error) {
|
||||
console.error("Error resolving finding IDs:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves check IDs into actual finding UUIDs.
|
||||
* Used at the group level where each row represents a check_id.
|
||||
*/
|
||||
export const resolveFindingIdsByCheckIds = async ({
|
||||
checkIds,
|
||||
}: {
|
||||
checkIds: string[];
|
||||
}): Promise<string[]> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/findings/latest`);
|
||||
url.searchParams.append("filter[check_id__in]", checkIds.join(","));
|
||||
url.searchParams.append("page[size]", "500");
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
const data = await handleApiResponse(response);
|
||||
|
||||
if (!data?.data || !Array.isArray(data.data)) return [];
|
||||
|
||||
return data.data.map((item: { id: string }) => item.id);
|
||||
} catch (error) {
|
||||
console.error("Error resolving finding IDs by check IDs:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestFindingsByResourceUid = async ({
|
||||
resourceUid,
|
||||
page = 1,
|
||||
|
||||
@@ -10,7 +10,15 @@ import { ContentLayout } from "@/components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AIChatbot() {
|
||||
export default async function AIChatbot({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const initialPrompt =
|
||||
typeof params.prompt === "string" ? params.prompt : undefined;
|
||||
|
||||
const hasConfig = await isLighthouseConfigured();
|
||||
|
||||
if (!hasConfig) {
|
||||
@@ -33,6 +41,7 @@ export default async function AIChatbot() {
|
||||
providers={providersConfig.providers}
|
||||
defaultProviderId={providersConfig.defaultProviderId}
|
||||
defaultModelId={providersConfig.defaultModelId}
|
||||
initialPrompt={initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
|
||||
import { MutedIcon } from "../icons";
|
||||
|
||||
@@ -14,10 +18,16 @@ export const Muted = ({
|
||||
if (isMuted === false) return null;
|
||||
|
||||
return (
|
||||
<Tooltip content={mutedReason} className="text-xs">
|
||||
<div className="border-system-severity-critical/40 w-fit rounded-full border p-1">
|
||||
<MutedIcon className="text-system-severity-critical h-4 w-4" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<MutedIcon className="text-text-neutral-primary size-2" />
|
||||
<span className="text-text-neutral-primary text-sm">Muted</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span className="text-xs">{mutedReason}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,8 +64,6 @@ export function getColumnFindingGroups({
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const group = row.original;
|
||||
const allMuted =
|
||||
group.mutedCount > 0 && group.mutedCount === group.resourcesTotal;
|
||||
|
||||
const delta =
|
||||
group.newCount > 0
|
||||
@@ -76,23 +74,18 @@ export function getColumnFindingGroups({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationIndicator delta={delta} isMuted={allMuted} />
|
||||
{group.resourcesTotal > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Expand ${group.checkTitle}`}
|
||||
className="hover:bg-bg-neutral-tertiary flex size-4 shrink-0 items-center justify-center rounded-md transition-colors"
|
||||
onClick={() => onDrillDown(group.checkId, group)}
|
||||
>
|
||||
<ChevronRight className="text-text-neutral-secondary size-4" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="w-4" />
|
||||
)}
|
||||
<NotificationIndicator delta={delta} />
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Expand ${group.checkTitle}`}
|
||||
className="hover:bg-bg-neutral-tertiary flex size-4 shrink-0 items-center justify-center rounded-md transition-colors"
|
||||
onClick={() => onDrillDown(group.checkId, group)}
|
||||
>
|
||||
<ChevronRight className="text-text-neutral-secondary size-4" />
|
||||
</button>
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={!!rowSelection[row.id]}
|
||||
disabled={allMuted}
|
||||
onCheckedChange={(checked) =>
|
||||
row.toggleSelected(checked === true)
|
||||
}
|
||||
@@ -128,18 +121,10 @@ export function getColumnFindingGroups({
|
||||
const group = row.original;
|
||||
|
||||
return (
|
||||
<div className="max-w-[500px]">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
"text-text-neutral-primary text-left text-sm break-words whitespace-normal",
|
||||
group.resourcesTotal > 1 &&
|
||||
"hover:text-button-tertiary cursor-pointer hover:underline",
|
||||
)}
|
||||
onClick={
|
||||
group.resourcesTotal > 1
|
||||
? () => onDrillDown(group.checkId, group)
|
||||
: undefined
|
||||
}
|
||||
className="text-text-neutral-primary hover:text-button-tertiary cursor-pointer text-left text-sm break-words whitespace-normal hover:underline"
|
||||
onClick={() => onDrillDown(group.checkId, group)}
|
||||
>
|
||||
{group.checkTitle}
|
||||
</p>
|
||||
|
||||
@@ -48,8 +48,10 @@ function getFailingForLabel(firstSeenAt: string | null): string | null {
|
||||
const ResourceRowActions = ({ row }: { row: Row<FindingResourceRow> }) => {
|
||||
const resource = row.original;
|
||||
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
|
||||
const [resolvedIds, setResolvedIds] = useState<string[]>([]);
|
||||
const [isResolving, setIsResolving] = useState(false);
|
||||
|
||||
const { selectedFindingIds, clearSelection } = useContext(
|
||||
const { selectedFindingIds, clearSelection, resolveMuteIds } = useContext(
|
||||
FindingsSelectionContext,
|
||||
) || {
|
||||
selectedFindingIds: [],
|
||||
@@ -59,7 +61,7 @@ const ResourceRowActions = ({ row }: { row: Row<FindingResourceRow> }) => {
|
||||
const isCurrentSelected = selectedFindingIds.includes(resource.findingId);
|
||||
const hasMultipleSelected = selectedFindingIds.length > 1;
|
||||
|
||||
const getMuteIds = (): string[] => {
|
||||
const getDisplayIds = (): string[] => {
|
||||
if (isCurrentSelected && hasMultipleSelected) {
|
||||
return selectedFindingIds;
|
||||
}
|
||||
@@ -68,20 +70,43 @@ const ResourceRowActions = ({ row }: { row: Row<FindingResourceRow> }) => {
|
||||
|
||||
const getMuteLabel = () => {
|
||||
if (resource.isMuted) return "Muted";
|
||||
const ids = getMuteIds();
|
||||
const ids = getDisplayIds();
|
||||
if (ids.length > 1) return `Mute ${ids.length}`;
|
||||
return "Mute";
|
||||
};
|
||||
|
||||
const handleMuteClick = async () => {
|
||||
const displayIds = getDisplayIds();
|
||||
|
||||
if (resolveMuteIds) {
|
||||
setIsResolving(true);
|
||||
const ids = await resolveMuteIds(displayIds);
|
||||
setResolvedIds(ids);
|
||||
setIsResolving(false);
|
||||
if (ids.length > 0) setIsMuteModalOpen(true);
|
||||
} else {
|
||||
setResolvedIds(displayIds);
|
||||
setIsMuteModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMuteComplete = () => {
|
||||
clearSelection();
|
||||
setResolvedIds([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MuteFindingsModal
|
||||
isOpen={isMuteModalOpen}
|
||||
onOpenChange={setIsMuteModalOpen}
|
||||
findingIds={getMuteIds()}
|
||||
onComplete={clearSelection}
|
||||
findingIds={resolvedIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
<div className="flex items-center justify-end">
|
||||
<div
|
||||
className="flex items-center justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ActionDropdown
|
||||
trigger={
|
||||
<button
|
||||
@@ -101,13 +126,15 @@ const ResourceRowActions = ({ row }: { row: Row<FindingResourceRow> }) => {
|
||||
icon={
|
||||
resource.isMuted ? (
|
||||
<VolumeOff className="size-5" />
|
||||
) : isResolving ? (
|
||||
<div className="size-5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<VolumeX className="size-5" />
|
||||
)
|
||||
}
|
||||
label={getMuteLabel()}
|
||||
disabled={resource.isMuted}
|
||||
onSelect={() => setIsMuteModalOpen(true)}
|
||||
label={isResolving ? "Resolving..." : getMuteLabel()}
|
||||
disabled={resource.isMuted || isResolving}
|
||||
onSelect={handleMuteClick}
|
||||
/>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
|
||||
@@ -68,41 +68,57 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
|
||||
// Get selection context - if there are other selected rows, include them
|
||||
const selectionContext = useContext(FindingsSelectionContext);
|
||||
const { selectedFindingIds, clearSelection } = selectionContext || {
|
||||
selectedFindingIds: [],
|
||||
clearSelection: () => {},
|
||||
};
|
||||
const { selectedFindingIds, clearSelection, resolveMuteIds } =
|
||||
selectionContext || {
|
||||
selectedFindingIds: [],
|
||||
clearSelection: () => {},
|
||||
};
|
||||
|
||||
const [resolvedIds, setResolvedIds] = useState<string[]>([]);
|
||||
const [isResolving, setIsResolving] = useState(false);
|
||||
|
||||
// If current finding is selected and there are multiple selections, mute all
|
||||
// Otherwise, just mute this single finding
|
||||
const isCurrentSelected = selectedFindingIds.includes(finding.id);
|
||||
const hasMultipleSelected = selectedFindingIds.length > 1;
|
||||
|
||||
const getMuteIds = (): string[] => {
|
||||
const getDisplayIds = (): string[] => {
|
||||
if (isCurrentSelected && hasMultipleSelected) {
|
||||
// Mute all selected including current
|
||||
return selectedFindingIds;
|
||||
}
|
||||
// Just mute the current finding
|
||||
return [finding.id];
|
||||
};
|
||||
|
||||
const getMuteLabel = () => {
|
||||
if (isMuted) return "Muted";
|
||||
const ids = getMuteIds();
|
||||
const ids = getDisplayIds();
|
||||
if (ids.length > 1) {
|
||||
return `Mute ${ids.length} Findings`;
|
||||
}
|
||||
return "Mute Finding";
|
||||
};
|
||||
|
||||
const handleMuteClick = async () => {
|
||||
const displayIds = getDisplayIds();
|
||||
|
||||
if (resolveMuteIds) {
|
||||
setIsResolving(true);
|
||||
const ids = await resolveMuteIds(displayIds);
|
||||
setResolvedIds(ids);
|
||||
setIsResolving(false);
|
||||
if (ids.length > 0) setIsMuteModalOpen(true);
|
||||
} else {
|
||||
// Regular findings — IDs are already valid finding UUIDs
|
||||
setResolvedIds(displayIds);
|
||||
setIsMuteModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMuteComplete = () => {
|
||||
// Always clear selection when a finding is muted because:
|
||||
// 1. If the muted finding was selected, its index now points to a different finding
|
||||
// 2. rowSelection uses indices (0, 1, 2...) not IDs, so after refresh the wrong findings would appear selected
|
||||
clearSelection();
|
||||
setResolvedIds([]);
|
||||
if (onMuteComplete) {
|
||||
onMuteComplete(getMuteIds());
|
||||
onMuteComplete(getDisplayIds());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +137,7 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
<MuteFindingsModal
|
||||
isOpen={isMuteModalOpen}
|
||||
onOpenChange={setIsMuteModalOpen}
|
||||
findingIds={getMuteIds()}
|
||||
findingIds={resolvedIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
|
||||
@@ -145,15 +161,15 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
icon={
|
||||
isMuted ? (
|
||||
<VolumeOff className="size-5" />
|
||||
) : isResolving ? (
|
||||
<div className="size-5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<VolumeX className="size-5" />
|
||||
)
|
||||
}
|
||||
label={getMuteLabel()}
|
||||
disabled={isMuted}
|
||||
onSelect={() => {
|
||||
setIsMuteModalOpen(true);
|
||||
}}
|
||||
label={isResolving ? "Resolving..." : getMuteLabel()}
|
||||
disabled={isMuted || isResolving}
|
||||
onSelect={handleMuteClick}
|
||||
/>
|
||||
<ActionDropdownItem
|
||||
icon={<JiraIcon size={20} />}
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
RowSelectionState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { ChevronLeft, VolumeX } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { resolveFindingIds } from "@/actions/findings/findings-by-resource";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner";
|
||||
import {
|
||||
Table,
|
||||
@@ -25,7 +27,7 @@ import { useInfiniteResources } from "@/hooks/use-infinite-resources";
|
||||
import { cn, hasDateOrScanFilter } from "@/lib";
|
||||
import { FindingGroupRow, FindingResourceRow } from "@/types";
|
||||
|
||||
import { FloatingMuteButton } from "../floating-mute-button";
|
||||
import { MuteFindingsModal } from "../mute-findings-modal";
|
||||
import { getColumnFindingResources } from "./column-finding-resources";
|
||||
import { FindingsSelectionContext } from "./findings-selection-context";
|
||||
import { ImpactedResourcesCell } from "./impacted-resources-cell";
|
||||
@@ -108,12 +110,42 @@ export function FindingsGroupDrillDown({
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
// Selection logic for resources
|
||||
// Selection logic — tracks by findingId (resource_id) for checkbox consistency
|
||||
const selectedFindingIds = Object.keys(rowSelection)
|
||||
.filter((key) => rowSelection[key])
|
||||
.map((idx) => resources[parseInt(idx)]?.findingId)
|
||||
.filter(Boolean);
|
||||
|
||||
// Mute modal state — resource IDs resolved to finding UUIDs on-click
|
||||
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
|
||||
const [resolvedFindingIds, setResolvedFindingIds] = useState<string[]>([]);
|
||||
const [isResolvingIds, setIsResolvingIds] = useState(false);
|
||||
|
||||
/** Converts resource_ids (display) → resourceUids → finding UUIDs via API. */
|
||||
const resolveResourceIds = useCallback(
|
||||
async (ids: string[]) => {
|
||||
const resourceUids = ids
|
||||
.map((id) => resources.find((r) => r.findingId === id)?.resourceUid)
|
||||
.filter(Boolean) as string[];
|
||||
if (resourceUids.length === 0) return [];
|
||||
return resolveFindingIds({
|
||||
checkId: group.checkId,
|
||||
resourceUids,
|
||||
});
|
||||
},
|
||||
[resources, group.checkId],
|
||||
);
|
||||
|
||||
const handleMuteClick = async () => {
|
||||
setIsResolvingIds(true);
|
||||
const findingIds = await resolveResourceIds(selectedFindingIds);
|
||||
setResolvedFindingIds(findingIds);
|
||||
setIsResolvingIds(false);
|
||||
if (findingIds.length > 0) {
|
||||
setIsMuteModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const selectableRowCount = resources.filter((r) => !r.isMuted).length;
|
||||
|
||||
const getRowCanSelect = (row: Row<FindingResourceRow>): boolean => {
|
||||
@@ -130,6 +162,7 @@ export function FindingsGroupDrillDown({
|
||||
|
||||
const handleMuteComplete = () => {
|
||||
clearSelection();
|
||||
setResolvedFindingIds([]);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
@@ -170,6 +203,7 @@ export function FindingsGroupDrillDown({
|
||||
selectedFindings: [],
|
||||
clearSelection,
|
||||
isSelected,
|
||||
resolveMuteIds: resolveResourceIds,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -282,11 +316,29 @@ export function FindingsGroupDrillDown({
|
||||
</div>
|
||||
|
||||
{selectedFindingIds.length > 0 && (
|
||||
<FloatingMuteButton
|
||||
selectedCount={selectedFindingIds.length}
|
||||
selectedFindingIds={selectedFindingIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
<>
|
||||
<MuteFindingsModal
|
||||
isOpen={isMuteModalOpen}
|
||||
onOpenChange={setIsMuteModalOpen}
|
||||
findingIds={resolvedFindingIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 fixed right-6 bottom-6 z-50 duration-300">
|
||||
<Button
|
||||
onClick={handleMuteClick}
|
||||
disabled={isResolvingIds}
|
||||
size="lg"
|
||||
className="shadow-lg"
|
||||
>
|
||||
{isResolvingIds ? (
|
||||
<TreeSpinner className="size-5" />
|
||||
) : (
|
||||
<VolumeX className="size-5" />
|
||||
)}
|
||||
Mute ({selectedFindingIds.length})
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ResourceDetailDrawer
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Row, RowSelectionState } from "@tanstack/react-table";
|
||||
import { VolumeX } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { resolveFindingIdsByCheckIds } from "@/actions/findings/findings-by-resource";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { FindingGroupRow, MetaDataProps } from "@/types";
|
||||
|
||||
import { FloatingMuteButton } from "../floating-mute-button";
|
||||
import { MuteFindingsModal } from "../mute-findings-modal";
|
||||
import { getColumnFindingGroups } from "./column-finding-groups";
|
||||
import { FindingsGroupDrillDown } from "./findings-group-drill-down";
|
||||
import { FindingsSelectionContext } from "./findings-selection-context";
|
||||
@@ -79,8 +83,32 @@ export function FindingsGroupTable({
|
||||
return selectedFindingIds.includes(id);
|
||||
};
|
||||
|
||||
// Mute modal state — check IDs resolved to finding UUIDs on-click
|
||||
const [isMuteModalOpen, setIsMuteModalOpen] = useState(false);
|
||||
const [resolvedFindingIds, setResolvedFindingIds] = useState<string[]>([]);
|
||||
const [isResolvingIds, setIsResolvingIds] = useState(false);
|
||||
|
||||
const handleMuteClick = async () => {
|
||||
setIsResolvingIds(true);
|
||||
const findingIds = await resolveFindingIdsByCheckIds({
|
||||
checkIds: selectedFindingIds,
|
||||
});
|
||||
setResolvedFindingIds(findingIds);
|
||||
setIsResolvingIds(false);
|
||||
if (findingIds.length > 0) {
|
||||
setIsMuteModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
/** Shared resolver for row action dropdowns (via context). */
|
||||
const resolveMuteIds = useCallback(
|
||||
async (checkIds: string[]) => resolveFindingIdsByCheckIds({ checkIds }),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMuteComplete = () => {
|
||||
clearSelection();
|
||||
setResolvedFindingIds([]);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
@@ -124,6 +152,7 @@ export function FindingsGroupTable({
|
||||
selectedFindings,
|
||||
clearSelection,
|
||||
isSelected,
|
||||
resolveMuteIds,
|
||||
}}
|
||||
>
|
||||
<DataTable
|
||||
@@ -140,11 +169,29 @@ export function FindingsGroupTable({
|
||||
/>
|
||||
|
||||
{selectedFindingIds.length > 0 && (
|
||||
<FloatingMuteButton
|
||||
selectedCount={selectedFindingIds.length}
|
||||
selectedFindingIds={selectedFindingIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
<>
|
||||
<MuteFindingsModal
|
||||
isOpen={isMuteModalOpen}
|
||||
onOpenChange={setIsMuteModalOpen}
|
||||
findingIds={resolvedFindingIds}
|
||||
onComplete={handleMuteComplete}
|
||||
/>
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 fixed right-6 bottom-6 z-50 duration-300">
|
||||
<Button
|
||||
onClick={handleMuteClick}
|
||||
disabled={isResolvingIds}
|
||||
size="lg"
|
||||
className="shadow-lg"
|
||||
>
|
||||
{isResolvingIds ? (
|
||||
<TreeSpinner className="size-5" />
|
||||
) : (
|
||||
<VolumeX className="size-5" />
|
||||
)}
|
||||
Mute ({selectedFindingIds.length})
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FindingsSelectionContext.Provider>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,11 @@ import { createContext, useContext } from "react";
|
||||
|
||||
interface FindingsSelectionContextValue {
|
||||
selectedFindingIds: string[];
|
||||
|
||||
selectedFindings: any[];
|
||||
clearSelection: () => void;
|
||||
isSelected: (id: string) => boolean;
|
||||
/** Resolves display IDs (check_ids or resource_ids) into real finding UUIDs for the mute API. */
|
||||
resolveMuteIds?: (ids: string[]) => Promise<string[]>;
|
||||
}
|
||||
|
||||
export const FindingsSelectionContext =
|
||||
|
||||
@@ -608,7 +608,7 @@ export function ResourceDetailDrawerContent({
|
||||
|
||||
{/* Lighthouse AI button */}
|
||||
<a
|
||||
href="/lighthouse"
|
||||
href={`/lighthouse?${new URLSearchParams({ prompt: `Analyze this security finding and provide remediation guidance:\n\n- **Finding**: ${checkMeta.checkTitle}\n- **Check ID**: ${checkMeta.checkId}\n- **Severity**: ${f?.severity ?? "unknown"}\n- **Status**: ${f?.status ?? "unknown"}${f?.statusExtended ? `\n- **Detail**: ${f.statusExtended}` : ""}${checkMeta.risk ? `\n- **Risk**: ${checkMeta.risk}` : ""}` }).toString()}`}
|
||||
className="flex items-center gap-1.5 rounded-lg px-4 py-3 text-sm font-bold text-slate-950 transition-opacity hover:opacity-90"
|
||||
style={{
|
||||
background: "linear-gradient(96deg, #2EE59B 3.55%, #62DFF0 98.85%)",
|
||||
|
||||
@@ -61,6 +61,7 @@ interface ChatProps {
|
||||
providers: Provider[];
|
||||
defaultProviderId?: LighthouseProvider;
|
||||
defaultModelId?: string;
|
||||
initialPrompt?: string;
|
||||
}
|
||||
|
||||
interface SelectedModel {
|
||||
@@ -102,6 +103,7 @@ export const Chat = ({
|
||||
providers: initialProviders,
|
||||
defaultProviderId,
|
||||
defaultModelId,
|
||||
initialPrompt,
|
||||
}: ChatProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -306,6 +308,15 @@ export const Chat = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-send initial prompt from URL (e.g., finding context from drawer)
|
||||
const initialPromptSentRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (initialPrompt && !initialPromptSentRef.current) {
|
||||
initialPromptSentRef.current = true;
|
||||
sendMessage({ text: initialPrompt });
|
||||
}
|
||||
}, [initialPrompt, sendMessage]);
|
||||
|
||||
// Handlers
|
||||
const handleNewChat = () => {
|
||||
setMessages([]);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useFilterTransitionOptional } from "@/contexts";
|
||||
|
||||
// const FINDINGS_PATH = "/findings";
|
||||
// const DEFAULT_MUTED_FILTER = "false";
|
||||
const FINDINGS_PATH = "/findings";
|
||||
const DEFAULT_MUTED_FILTER = "false";
|
||||
|
||||
/**
|
||||
* Custom hook to handle URL filters and automatically reset
|
||||
@@ -22,12 +22,10 @@ export const useUrlFilters = () => {
|
||||
const isPending = false;
|
||||
|
||||
const ensureFindingsDefaultMuted = (params: URLSearchParams) => {
|
||||
// TODO: Re-enable when finding-groups endpoint supports filter[muted]
|
||||
// Findings defaults to excluding muted findings unless user sets it explicitly.
|
||||
// if (pathname === FINDINGS_PATH && !params.has("filter[muted]")) {
|
||||
// params.set("filter[muted]", DEFAULT_MUTED_FILTER);
|
||||
// }
|
||||
void params;
|
||||
if (pathname === FINDINGS_PATH && !params.has("filter[muted]")) {
|
||||
params.set("filter[muted]", DEFAULT_MUTED_FILTER);
|
||||
}
|
||||
};
|
||||
|
||||
const navigate = (params: URLSearchParams) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ export const getMenuList = ({ pathname }: MenuListOptions): GroupProps[] => {
|
||||
groupLabel: "",
|
||||
menus: [
|
||||
{
|
||||
href: "/findings",
|
||||
href: "/findings?filter[muted]=false",
|
||||
label: "Findings",
|
||||
icon: Tag,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user