Skip to content

Commit a07e599

Browse files
feat(ui): add service watchlist component with real API integration (#9316)
Co-authored-by: alejandrobailo <[email protected]>
1 parent e020b3f commit a07e599

File tree

11 files changed

+157
-66
lines changed

11 files changed

+157
-66
lines changed

ui/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ All notable changes to the **Prowler UI** are documented in this file.
1515
- External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151)
1616
- New Overview page and new app styles [(#9234)](https://github.com/prowler-cloud/prowler/pull/9234)
1717
- Use branch name as region for IaC findings [(#9296)](https://github.com/prowler-cloud/prowler/pull/9296)
18+
- Compliance Watchlist component to Overview page [(#9199)](https://github.com/prowler-cloud/prowler/pull/9199)
19+
- Service Watchlist component to Overview page [(#9316)](https://github.com/prowler-cloud/prowler/pull/9316)
1820

1921
### 🔄 Changed
2022

ui/actions/overview/overview.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ import { redirect } from "next/navigation";
44
import { apiBaseUrl, getAuthHeaders } from "@/lib";
55
import { handleApiResponse } from "@/lib/server-actions-helper";
66

7+
import { ServicesOverviewResponse } from "./types";
8+
9+
export const getServicesOverview = async ({
10+
filters = {},
11+
}: {
12+
filters?: Record<string, string | string[] | undefined>;
13+
} = {}): Promise<ServicesOverviewResponse | undefined> => {
14+
const headers = await getAuthHeaders({ contentType: false });
15+
16+
const url = new URL(`${apiBaseUrl}/overviews/services`);
17+
18+
// Handle multiple filters
19+
Object.entries(filters).forEach(([key, value]) => {
20+
if (key !== "filter[search]" && value !== undefined) {
21+
url.searchParams.append(key, String(value));
22+
}
23+
});
24+
25+
try {
26+
const response = await fetch(url.toString(), {
27+
headers,
28+
});
29+
30+
return handleApiResponse(response);
31+
} catch (error) {
32+
console.error("Error fetching services overview:", error);
33+
return undefined;
34+
}
35+
};
36+
737
export const getProvidersOverview = async ({
838
page = 1,
939
query = "",

ui/actions/overview/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
// Services Overview Types
2+
// Corresponds to the /overviews/services endpoint
3+
4+
export interface ServiceOverviewAttributes {
5+
total: number;
6+
fail: number;
7+
muted: number;
8+
pass: number;
9+
}
10+
11+
export interface ServiceOverview {
12+
type: "services-overview";
13+
id: string;
14+
attributes: ServiceOverviewAttributes;
15+
}
16+
17+
export interface ServicesOverviewResponse {
18+
data: ServiceOverview[];
19+
meta: {
20+
version: string;
21+
};
22+
}
23+
124
// ThreatScore Snapshot Types
225
// Corresponds to the ThreatScoreSnapshot model from the API
326

ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.ssr.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export const ComplianceWatchlistSSR = async ({
1717
const response = await getCompliancesOverview({ filters });
1818
const { data } = adaptComplianceOverviewsResponse(response);
1919

20-
// Filter out ProwlerThreatScore and limit to 9 items
20+
// Filter out ProwlerThreatScore and limit to 5 items
2121
const items = data
2222
.filter((item) => item.framework !== "ProwlerThreatScore")
23-
.slice(0, 9)
23+
.slice(0, 5)
2424
.map((compliance) => ({
2525
id: compliance.id,
2626
framework: compliance.framework,

ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"use client";
22

3-
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from "lucide-react";
43
import Image, { type StaticImageData } from "next/image";
54
import { useState } from "react";
65

7-
import { Button } from "@/components/shadcn/button/button";
8-
6+
import { SortToggleButton } from "./sort-toggle-button";
97
import { WatchlistCard } from "./watchlist-card";
108

119
export interface ComplianceData {
@@ -39,23 +37,19 @@ export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
3937
value: `${item.score}%`,
4038
}));
4139

42-
const SortIcon = isAsc ? ArrowUpNarrowWide : ArrowDownNarrowWide;
43-
4440
return (
4541
<WatchlistCard
4642
title="Compliance Watchlist"
4743
items={sortedItems}
4844
ctaLabel="Compliance Dashboard"
4945
ctaHref="/compliance"
5046
headerAction={
51-
<Button
52-
variant="ghost"
53-
size="icon"
54-
onClick={() => setIsAsc(!isAsc)}
55-
aria-label={isAsc ? "Sort by highest score" : "Sort by lowest score"}
56-
>
57-
<SortIcon className="size-4" />
58-
</Button>
47+
<SortToggleButton
48+
isAscending={isAsc}
49+
onToggle={() => setIsAsc(!isAsc)}
50+
ascendingLabel="Sort by highest score"
51+
descendingLabel="Sort by lowest score"
52+
/>
5953
}
6054
emptyState={{
6155
message: "This space is looking empty.",
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export type { ComplianceData } from "./compliance-watchlist";
22
export { ComplianceWatchlist } from "./compliance-watchlist";
33
export { ComplianceWatchlistSSR } from "./compliance-watchlist.ssr";
4-
export * from "./service-watchlist";
4+
export { ServiceWatchlist } from "./service-watchlist";
5+
export { ServiceWatchlistSSR } from "./service-watchlist.ssr";
6+
export { SortToggleButton } from "./sort-toggle-button";
57
export * from "./watchlist-card";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getServicesOverview, ServiceOverview } from "@/actions/overview";
2+
import { SearchParamsProps } from "@/types";
3+
4+
import { pickFilterParams } from "../../lib/filter-params";
5+
import { ServiceWatchlist } from "./service-watchlist";
6+
7+
export const ServiceWatchlistSSR = async ({
8+
searchParams,
9+
}: {
10+
searchParams: SearchParamsProps | undefined | null;
11+
}) => {
12+
const filters = pickFilterParams(searchParams);
13+
14+
const response = await getServicesOverview({ filters });
15+
16+
const items: ServiceOverview[] = response?.data ?? [];
17+
18+
return <ServiceWatchlist items={items} />;
19+
};

ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx

Lines changed: 30 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,43 @@
11
"use client";
22

3-
import { getAWSIcon } from "@/components/icons/services/IconServices";
3+
import { useState } from "react";
44

5-
import { WatchlistCard, WatchlistItem } from "./watchlist-card";
5+
import { ServiceOverview } from "@/actions/overview";
66

7-
const MOCK_SERVICE_ITEMS: WatchlistItem[] = [
8-
{
9-
key: "amazon-s3-1",
10-
icon: getAWSIcon("Amazon S3"),
11-
label: "Amazon S3",
12-
value: "5",
13-
},
14-
{
15-
key: "amazon-ec2",
16-
icon: getAWSIcon("Amazon EC2"),
17-
label: "Amazon EC2",
18-
value: "8",
19-
},
20-
{
21-
key: "amazon-rds",
22-
icon: getAWSIcon("Amazon RDS"),
23-
label: "Amazon RDS",
24-
value: "12",
25-
},
26-
{
27-
key: "aws-iam",
28-
icon: getAWSIcon("AWS IAM"),
29-
label: "AWS IAM",
30-
value: "15",
31-
},
32-
{
33-
key: "aws-lambda",
34-
icon: getAWSIcon("AWS Lambda"),
35-
label: "AWS Lambda",
36-
value: "22",
37-
},
38-
{
39-
key: "amazon-vpc",
40-
icon: getAWSIcon("Amazon VPC"),
41-
label: "Amazon VPC",
42-
value: "28",
43-
},
44-
{
45-
key: "amazon-cloudwatch",
46-
icon: getAWSIcon("AWS CloudWatch"),
47-
label: "AWS CloudWatch",
48-
value: "78",
49-
},
50-
];
7+
import { SortToggleButton } from "./sort-toggle-button";
8+
import { WatchlistCard } from "./watchlist-card";
9+
10+
export const ServiceWatchlist = ({ items }: { items: ServiceOverview[] }) => {
11+
const [isAsc, setIsAsc] = useState(true);
12+
13+
const sortedItems = [...items]
14+
.sort((a, b) =>
15+
isAsc
16+
? a.attributes.fail - b.attributes.fail
17+
: b.attributes.fail - a.attributes.fail,
18+
)
19+
.slice(0, 5)
20+
.map((item) => ({
21+
key: item.id,
22+
icon: <div className="bg-bg-data-muted size-3 rounded-sm" />,
23+
label: item.id,
24+
value: item.attributes.fail,
25+
}));
5126

52-
export const ServiceWatchlist = () => {
5327
return (
5428
<WatchlistCard
5529
title="Service Watchlist"
56-
items={MOCK_SERVICE_ITEMS}
30+
items={sortedItems}
5731
ctaLabel="Services Dashboard"
5832
ctaHref="/services"
33+
headerAction={
34+
<SortToggleButton
35+
isAscending={isAsc}
36+
onToggle={() => setIsAsc(!isAsc)}
37+
ascendingLabel="Sort by highest failures"
38+
descendingLabel="Sort by lowest failures"
39+
/>
40+
}
5941
emptyState={{
6042
message: "This space is looking empty.",
6143
description: "to add services to your watchlist.",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from "lucide-react";
4+
5+
import { Button } from "@/components/shadcn/button/button";
6+
7+
interface SortToggleButtonProps {
8+
isAscending: boolean;
9+
onToggle: () => void;
10+
ascendingLabel?: string;
11+
descendingLabel?: string;
12+
}
13+
14+
export const SortToggleButton = ({
15+
isAscending,
16+
onToggle,
17+
ascendingLabel = "Sort descending",
18+
descendingLabel = "Sort ascending",
19+
}: SortToggleButtonProps) => {
20+
const SortIcon = isAscending ? ArrowUpNarrowWide : ArrowDownNarrowWide;
21+
22+
return (
23+
<Button
24+
variant="ghost"
25+
size="icon"
26+
onClick={onToggle}
27+
aria-label={isAscending ? ascendingLabel : descendingLabel}
28+
>
29+
<SortIcon className="size-4" />
30+
</Button>
31+
);
32+
};

ui/app/(prowler)/_new-overview/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { StatusChartSkeleton } from "./components/status-chart";
1818
import { ThreatScoreSkeleton, ThreatScoreSSR } from "./components/threat-score";
1919
import {
2020
ComplianceWatchlistSSR,
21-
ServiceWatchlist,
21+
ServiceWatchlistSSR,
2222
WatchlistCardSkeleton,
2323
} from "./components/watchlist";
2424

@@ -64,7 +64,9 @@ export default async function NewOverviewPage({
6464
</Suspense>
6565
</div>
6666
<div className="mt-6 flex gap-6">
67-
<ServiceWatchlist />
67+
<Suspense fallback={<WatchlistCardSkeleton />}>
68+
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
69+
</Suspense>
6870
<GraphsTabsWrapper searchParams={resolvedSearchParams} />
6971
</div>
7072
</ContentLayout>

0 commit comments

Comments
 (0)