mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(ui): add service watchlist component with real API integration (#9316)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
@@ -15,6 +15,8 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151)
|
||||
- New Overview page and new app styles [(#9234)](https://github.com/prowler-cloud/prowler/pull/9234)
|
||||
- Use branch name as region for IaC findings [(#9296)](https://github.com/prowler-cloud/prowler/pull/9296)
|
||||
- Compliance Watchlist component to Overview page [(#9199)](https://github.com/prowler-cloud/prowler/pull/9199)
|
||||
- Service Watchlist component to Overview page [(#9316)](https://github.com/prowler-cloud/prowler/pull/9316)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -4,6 +4,36 @@ import { redirect } from "next/navigation";
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ServicesOverviewResponse } from "./types";
|
||||
|
||||
export const getServicesOverview = async ({
|
||||
filters = {},
|
||||
}: {
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}): Promise<ServicesOverviewResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/services`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching services overview:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProvidersOverview = async ({
|
||||
page = 1,
|
||||
query = "",
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
// Services Overview Types
|
||||
// Corresponds to the /overviews/services endpoint
|
||||
|
||||
export interface ServiceOverviewAttributes {
|
||||
total: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
pass: number;
|
||||
}
|
||||
|
||||
export interface ServiceOverview {
|
||||
type: "services-overview";
|
||||
id: string;
|
||||
attributes: ServiceOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ServicesOverviewResponse {
|
||||
data: ServiceOverview[];
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ThreatScore Snapshot Types
|
||||
// Corresponds to the ThreatScoreSnapshot model from the API
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ export const ComplianceWatchlistSSR = async ({
|
||||
const response = await getCompliancesOverview({ filters });
|
||||
const { data } = adaptComplianceOverviewsResponse(response);
|
||||
|
||||
// Filter out ProwlerThreatScore and limit to 9 items
|
||||
// Filter out ProwlerThreatScore and limit to 5 items
|
||||
const items = data
|
||||
.filter((item) => item.framework !== "ProwlerThreatScore")
|
||||
.slice(0, 9)
|
||||
.slice(0, 5)
|
||||
.map((compliance) => ({
|
||||
id: compliance.id,
|
||||
framework: compliance.framework,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from "lucide-react";
|
||||
import Image, { type StaticImageData } from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
|
||||
import { SortToggleButton } from "./sort-toggle-button";
|
||||
import { WatchlistCard } from "./watchlist-card";
|
||||
|
||||
export interface ComplianceData {
|
||||
@@ -39,8 +37,6 @@ export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
value: `${item.score}%`,
|
||||
}));
|
||||
|
||||
const SortIcon = isAsc ? ArrowUpNarrowWide : ArrowDownNarrowWide;
|
||||
|
||||
return (
|
||||
<WatchlistCard
|
||||
title="Compliance Watchlist"
|
||||
@@ -48,14 +44,12 @@ export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
ctaLabel="Compliance Dashboard"
|
||||
ctaHref="/compliance"
|
||||
headerAction={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsAsc(!isAsc)}
|
||||
aria-label={isAsc ? "Sort by highest score" : "Sort by lowest score"}
|
||||
>
|
||||
<SortIcon className="size-4" />
|
||||
</Button>
|
||||
<SortToggleButton
|
||||
isAscending={isAsc}
|
||||
onToggle={() => setIsAsc(!isAsc)}
|
||||
ascendingLabel="Sort by highest score"
|
||||
descendingLabel="Sort by lowest score"
|
||||
/>
|
||||
}
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export type { ComplianceData } from "./compliance-watchlist";
|
||||
export { ComplianceWatchlist } from "./compliance-watchlist";
|
||||
export { ComplianceWatchlistSSR } from "./compliance-watchlist.ssr";
|
||||
export * from "./service-watchlist";
|
||||
export { ServiceWatchlist } from "./service-watchlist";
|
||||
export { ServiceWatchlistSSR } from "./service-watchlist.ssr";
|
||||
export { SortToggleButton } from "./sort-toggle-button";
|
||||
export * from "./watchlist-card";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getServicesOverview, ServiceOverview } from "@/actions/overview";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { pickFilterParams } from "../../lib/filter-params";
|
||||
import { ServiceWatchlist } from "./service-watchlist";
|
||||
|
||||
export const ServiceWatchlistSSR = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps | undefined | null;
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const response = await getServicesOverview({ filters });
|
||||
|
||||
const items: ServiceOverview[] = response?.data ?? [];
|
||||
|
||||
return <ServiceWatchlist items={items} />;
|
||||
};
|
||||
@@ -1,61 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { getAWSIcon } from "@/components/icons/services/IconServices";
|
||||
import { useState } from "react";
|
||||
|
||||
import { WatchlistCard, WatchlistItem } from "./watchlist-card";
|
||||
import { ServiceOverview } from "@/actions/overview";
|
||||
|
||||
const MOCK_SERVICE_ITEMS: WatchlistItem[] = [
|
||||
{
|
||||
key: "amazon-s3-1",
|
||||
icon: getAWSIcon("Amazon S3"),
|
||||
label: "Amazon S3",
|
||||
value: "5",
|
||||
},
|
||||
{
|
||||
key: "amazon-ec2",
|
||||
icon: getAWSIcon("Amazon EC2"),
|
||||
label: "Amazon EC2",
|
||||
value: "8",
|
||||
},
|
||||
{
|
||||
key: "amazon-rds",
|
||||
icon: getAWSIcon("Amazon RDS"),
|
||||
label: "Amazon RDS",
|
||||
value: "12",
|
||||
},
|
||||
{
|
||||
key: "aws-iam",
|
||||
icon: getAWSIcon("AWS IAM"),
|
||||
label: "AWS IAM",
|
||||
value: "15",
|
||||
},
|
||||
{
|
||||
key: "aws-lambda",
|
||||
icon: getAWSIcon("AWS Lambda"),
|
||||
label: "AWS Lambda",
|
||||
value: "22",
|
||||
},
|
||||
{
|
||||
key: "amazon-vpc",
|
||||
icon: getAWSIcon("Amazon VPC"),
|
||||
label: "Amazon VPC",
|
||||
value: "28",
|
||||
},
|
||||
{
|
||||
key: "amazon-cloudwatch",
|
||||
icon: getAWSIcon("AWS CloudWatch"),
|
||||
label: "AWS CloudWatch",
|
||||
value: "78",
|
||||
},
|
||||
];
|
||||
import { SortToggleButton } from "./sort-toggle-button";
|
||||
import { WatchlistCard } from "./watchlist-card";
|
||||
|
||||
export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => {
|
||||
const [isAsc, setIsAsc] = useState(true);
|
||||
|
||||
const sortedItems = [...items]
|
||||
.sort((a, b) =>
|
||||
isAsc
|
||||
? a.attributes.fail - b.attributes.fail
|
||||
: b.attributes.fail - a.attributes.fail,
|
||||
)
|
||||
.slice(0, 5)
|
||||
.map((item) => ({
|
||||
key: item.id,
|
||||
icon: <div className="bg-bg-data-muted size-3 rounded-sm" />,
|
||||
label: item.id,
|
||||
value: item.attributes.fail,
|
||||
}));
|
||||
|
||||
export const ServiceWatchlist = () => {
|
||||
return (
|
||||
<WatchlistCard
|
||||
title="Service Watchlist"
|
||||
items={MOCK_SERVICE_ITEMS}
|
||||
items={sortedItems}
|
||||
ctaLabel="Services Dashboard"
|
||||
ctaHref="/services"
|
||||
headerAction={
|
||||
<SortToggleButton
|
||||
isAscending={isAsc}
|
||||
onToggle={() => setIsAsc(!isAsc)}
|
||||
ascendingLabel="Sort by highest failures"
|
||||
descendingLabel="Sort by lowest failures"
|
||||
/>
|
||||
}
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
description: "to add services to your watchlist.",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
|
||||
interface SortToggleButtonProps {
|
||||
isAscending: boolean;
|
||||
onToggle: () => void;
|
||||
ascendingLabel?: string;
|
||||
descendingLabel?: string;
|
||||
}
|
||||
|
||||
export const SortToggleButton = ({
|
||||
isAscending,
|
||||
onToggle,
|
||||
ascendingLabel = "Sort descending",
|
||||
descendingLabel = "Sort ascending",
|
||||
}: SortToggleButtonProps) => {
|
||||
const SortIcon = isAscending ? ArrowUpNarrowWide : ArrowDownNarrowWide;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggle}
|
||||
aria-label={isAscending ? ascendingLabel : descendingLabel}
|
||||
>
|
||||
<SortIcon className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ import { StatusChartSkeleton } from "./components/status-chart";
|
||||
import { ThreatScoreSkeleton, ThreatScoreSSR } from "./components/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
ServiceWatchlist,
|
||||
ServiceWatchlistSSR,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./components/watchlist";
|
||||
|
||||
@@ -64,7 +64,9 @@ export default async function NewOverviewPage({
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="mt-6 flex gap-6">
|
||||
<ServiceWatchlist />
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
<GraphsTabsWrapper searchParams={resolvedSearchParams} />
|
||||
</div>
|
||||
</ContentLayout>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "./_new-overview/components/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
ServiceWatchlistSSR,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./_new-overview/components/watchlist";
|
||||
|
||||
@@ -62,6 +63,10 @@ export default async function Home({
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<ThreatScoreSkeleton />}>
|
||||
<ThreatScoreSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
Reference in New Issue
Block a user