diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index f3bf2a26bc..48130e3b7d 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added +- Finding Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405) - Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412) - 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) diff --git a/ui/actions/api-keys/index.ts b/ui/actions/api-keys/index.ts new file mode 100644 index 0000000000..51e41f6e13 --- /dev/null +++ b/ui/actions/api-keys/index.ts @@ -0,0 +1,2 @@ +export * from "./api-keys"; +export * from "./api-keys.adapter"; diff --git a/ui/actions/compliances/compliances.adapter.ts b/ui/actions/compliances/compliances.adapter.ts index b1bb821bf9..25479c0b0e 100644 --- a/ui/actions/compliances/compliances.adapter.ts +++ b/ui/actions/compliances/compliances.adapter.ts @@ -1,38 +1,12 @@ -import { StaticImageData } from "next/image"; - import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance"; import { MetaDataProps } from "@/types"; -import { ComplianceOverviewData } from "@/types/compliance"; -/** - * Raw API response from /compliance-overviews endpoint - */ -export interface ComplianceOverviewsResponse { - data: ComplianceOverviewData[]; - meta?: { - pagination?: { - page: number; - pages: number; - count: number; - }; - }; -} +import { + ComplianceOverviewsResponse, + EnrichedComplianceOverview, +} from "./types"; -/** - * Enriched compliance overview with computed fields - */ -export interface EnrichedComplianceOverview { - id: string; - framework: string; - version: string; - requirements_passed: number; - requirements_failed: number; - requirements_manual: number; - total_requirements: number; - score: number; - label: string; - icon: string | StaticImageData | undefined; -} +export type { ComplianceOverviewsResponse, EnrichedComplianceOverview }; /** * Formats framework name for display by replacing hyphens with spaces diff --git a/ui/actions/compliances/index.ts b/ui/actions/compliances/index.ts index 0ea27fabf5..77a4046d1a 100644 --- a/ui/actions/compliances/index.ts +++ b/ui/actions/compliances/index.ts @@ -1,2 +1,6 @@ export * from "./compliances"; export * from "./compliances.adapter"; +export type { + ComplianceOverviewsResponse, + EnrichedComplianceOverview, +} from "./types"; diff --git a/ui/actions/compliances/types.ts b/ui/actions/compliances/types.ts new file mode 100644 index 0000000000..c20f3a1fe3 --- /dev/null +++ b/ui/actions/compliances/types.ts @@ -0,0 +1,33 @@ +import { StaticImageData } from "next/image"; + +import { ComplianceOverviewData } from "@/types/compliance"; + +/** + * Raw API response from /compliance-overviews endpoint + */ +export interface ComplianceOverviewsResponse { + data: ComplianceOverviewData[]; + meta?: { + pagination?: { + page: number; + pages: number; + count: number; + }; + }; +} + +/** + * Enriched compliance overview with computed fields + */ +export interface EnrichedComplianceOverview { + id: string; + framework: string; + version: string; + requirements_passed: number; + requirements_failed: number; + requirements_manual: number; + total_requirements: number; + score: number; + label: string; + icon: string | StaticImageData | undefined; +} diff --git a/ui/actions/overview/attack-surface.adapter.ts b/ui/actions/overview/attack-surface/attack-surface.adapter.ts similarity index 96% rename from ui/actions/overview/attack-surface.adapter.ts rename to ui/actions/overview/attack-surface/attack-surface.adapter.ts index b28a98b4f4..9d15efb1da 100644 --- a/ui/actions/overview/attack-surface.adapter.ts +++ b/ui/actions/overview/attack-surface/attack-surface.adapter.ts @@ -1,7 +1,4 @@ -import { - AttackSurfaceOverview, - AttackSurfaceOverviewResponse, -} from "./types/attack-surface"; +import { AttackSurfaceOverview, AttackSurfaceOverviewResponse } from "../types"; const ATTACK_SURFACE_IDS = { INTERNET_EXPOSED: "internet-exposed", diff --git a/ui/actions/overview/attack-surface/attack-surface.ts b/ui/actions/overview/attack-surface/attack-surface.ts new file mode 100644 index 0000000000..b1af01a022 --- /dev/null +++ b/ui/actions/overview/attack-surface/attack-surface.ts @@ -0,0 +1,34 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { AttackSurfaceOverviewResponse } from "../types"; + +export const getAttackSurfaceOverview = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`); + + // 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 attack surface overview:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/attack-surface/index.ts b/ui/actions/overview/attack-surface/index.ts new file mode 100644 index 0000000000..e414f53f67 --- /dev/null +++ b/ui/actions/overview/attack-surface/index.ts @@ -0,0 +1,2 @@ +export * from "./attack-surface"; +export * from "./attack-surface.adapter"; diff --git a/ui/actions/overview/findings/findings.ts b/ui/actions/overview/findings/findings.ts new file mode 100644 index 0000000000..36ff241f95 --- /dev/null +++ b/ui/actions/overview/findings/findings.ts @@ -0,0 +1,86 @@ +"use server"; + +import { redirect } from "next/navigation"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { FindingsSeverityOverviewResponse } from "../types"; + +export const getFindingsByStatus = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}: { + page?: number; + query?: string; + sort?: string; + filters?: Record; +} = {}) => { + const headers = await getAuthHeaders({ contentType: false }); + + if (isNaN(Number(page)) || page < 1) redirect("/"); + + const url = new URL(`${apiBaseUrl}/overviews/findings`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it + Object.entries(filters).forEach(([key, value]) => { + // The overviews/findings endpoint does not support status or muted filters + // (allowed filters include date, region, provider fields). Exclude unsupported ones. + if ( + key !== "filter[search]" && + key !== "filter[muted]" && + key !== "filter[status]" + ) { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers, + }); + + return handleApiResponse(response); + } catch (error) { + console.error("Error fetching findings severity overview:", error); + return undefined; + } +}; + +export const getFindingsBySeverity = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/findings_severity`); + + // Handle multiple filters, but exclude unsupported filters + Object.entries(filters).forEach(([key, value]) => { + if ( + key !== "filter[search]" && + key !== "filter[muted]" && + 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 findings severity overview:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/findings/index.ts b/ui/actions/overview/findings/index.ts new file mode 100644 index 0000000000..eb3a674c67 --- /dev/null +++ b/ui/actions/overview/findings/index.ts @@ -0,0 +1 @@ +export * from "./findings"; diff --git a/ui/actions/overview/index.ts b/ui/actions/overview/index.ts index ab7cba34a0..844ef3d410 100644 --- a/ui/actions/overview/index.ts +++ b/ui/actions/overview/index.ts @@ -1,5 +1,9 @@ -export * from "./attack-surface.adapter"; -export * from "./overview"; -export * from "./sankey.adapter"; -export * from "./threat-map.adapter"; +// Re-export all overview actions from feature-based subfolders +export * from "./attack-surface"; +export * from "./findings"; +export * from "./providers"; +export * from "./regions"; +export * from "./services"; +export * from "./severity-trends"; +export * from "./threat-score"; export * from "./types"; diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts deleted file mode 100644 index fed9c8ac14..0000000000 --- a/ui/actions/overview/overview.ts +++ /dev/null @@ -1,243 +0,0 @@ -"use server"; -import { redirect } from "next/navigation"; - -import { apiBaseUrl, getAuthHeaders } from "@/lib"; -import { handleApiResponse } from "@/lib/server-actions-helper"; - -import { - AttackSurfaceOverviewResponse, - FindingsSeverityOverviewResponse, - ProvidersOverviewResponse, - RegionsOverviewResponse, - ServicesOverviewResponse, -} from "./types"; - -export const getServicesOverview = async ({ - filters = {}, -}: { - filters?: Record; -} = {}): Promise => { - 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 = "", - sort = "", - filters = {}, -}: { - page?: number; - query?: string; - sort?: string; - filters?: Record; -} = {}): Promise => { - const headers = await getAuthHeaders({ contentType: false }); - - if (isNaN(Number(page)) || page < 1) redirect("/providers-overview"); - - const url = new URL(`${apiBaseUrl}/overviews/providers`); - - if (page) url.searchParams.append("page[number]", page.toString()); - if (query) url.searchParams.append("filter[search]", query); - if (sort) url.searchParams.append("sort", sort); - - // 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 providers overview:", error); - return undefined; - } -}; - -export const getFindingsByStatus = async ({ - page = 1, - query = "", - sort = "", - filters = {}, -}: { - page?: number; - query?: string; - sort?: string; - filters?: Record; -} = {}) => { - const headers = await getAuthHeaders({ contentType: false }); - - if (isNaN(Number(page)) || page < 1) redirect("/"); - - const url = new URL(`${apiBaseUrl}/overviews/findings`); - - if (page) url.searchParams.append("page[number]", page.toString()); - if (query) url.searchParams.append("filter[search]", query); - if (sort) url.searchParams.append("sort", sort); - - // Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it - Object.entries(filters).forEach(([key, value]) => { - // The overviews/findings endpoint does not support status or muted filters - // (allowed filters include date, region, provider fields). Exclude unsupported ones. - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - key !== "filter[status]" - ) { - url.searchParams.append(key, String(value)); - } - }); - - try { - const response = await fetch(url.toString(), { - headers, - }); - - return handleApiResponse(response); - } catch (error) { - console.error("Error fetching findings severity overview:", error); - return undefined; - } -}; - -export const getFindingsBySeverity = async ({ - filters = {}, -}: { - filters?: Record; -} = {}): Promise => { - const headers = await getAuthHeaders({ contentType: false }); - - const url = new URL(`${apiBaseUrl}/overviews/findings_severity`); - - // Handle multiple filters, but exclude unsupported filters - Object.entries(filters).forEach(([key, value]) => { - if ( - key !== "filter[search]" && - key !== "filter[muted]" && - 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 findings severity overview:", error); - return undefined; - } -}; - -export const getThreatScore = async ({ - filters = {}, -}: { - filters?: Record; -} = {}) => { - const headers = await getAuthHeaders({ contentType: false }); - - const url = new URL(`${apiBaseUrl}/overviews/threatscore`); - - // Handle multiple filters - Object.entries(filters).forEach(([key, value]) => { - if (key !== "filter[search]") { - url.searchParams.append(key, String(value)); - } - }); - - try { - const response = await fetch(url.toString(), { - headers, - }); - - return handleApiResponse(response); - } catch (error) { - console.error("Error fetching threat score:", error); - return undefined; - } -}; - -export const getRegionsOverview = async ({ - filters = {}, -}: { - filters?: Record; -} = {}): Promise => { - const headers = await getAuthHeaders({ contentType: false }); - - const url = new URL(`${apiBaseUrl}/overviews/regions`); - - // 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 regions overview:", error); - return undefined; - } -}; - -export const getAttackSurfaceOverview = async ({ - filters = {}, -}: { - filters?: Record; -} = {}): Promise => { - const headers = await getAuthHeaders({ contentType: false }); - - const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`); - - // 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 attack surface overview:", error); - return undefined; - } -}; diff --git a/ui/actions/overview/providers/index.ts b/ui/actions/overview/providers/index.ts new file mode 100644 index 0000000000..be96de17d7 --- /dev/null +++ b/ui/actions/overview/providers/index.ts @@ -0,0 +1,2 @@ +export * from "./providers"; +export * from "./sankey.adapter"; diff --git a/ui/actions/overview/providers/providers.ts b/ui/actions/overview/providers/providers.ts new file mode 100644 index 0000000000..42968acb32 --- /dev/null +++ b/ui/actions/overview/providers/providers.ts @@ -0,0 +1,47 @@ +"use server"; + +import { redirect } from "next/navigation"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { ProvidersOverviewResponse } from "../types"; + +export const getProvidersOverview = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}: { + page?: number; + query?: string; + sort?: string; + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + if (isNaN(Number(page)) || page < 1) redirect("/providers-overview"); + + const url = new URL(`${apiBaseUrl}/overviews/providers`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + 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 providers overview:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/sankey.adapter.ts b/ui/actions/overview/providers/sankey.adapter.ts similarity index 100% rename from ui/actions/overview/sankey.adapter.ts rename to ui/actions/overview/providers/sankey.adapter.ts diff --git a/ui/actions/overview/regions/index.ts b/ui/actions/overview/regions/index.ts new file mode 100644 index 0000000000..a87c31fd35 --- /dev/null +++ b/ui/actions/overview/regions/index.ts @@ -0,0 +1,2 @@ +export * from "./regions"; +export * from "./threat-map.adapter"; diff --git a/ui/actions/overview/regions/regions.ts b/ui/actions/overview/regions/regions.ts new file mode 100644 index 0000000000..f478ed23d5 --- /dev/null +++ b/ui/actions/overview/regions/regions.ts @@ -0,0 +1,34 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { RegionsOverviewResponse } from "../types"; + +export const getRegionsOverview = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/regions`); + + // 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 regions overview:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/threat-map.adapter.ts b/ui/actions/overview/regions/threat-map.adapter.ts similarity index 99% rename from ui/actions/overview/threat-map.adapter.ts rename to ui/actions/overview/regions/threat-map.adapter.ts index 8b0c60b844..e3b9bc0a02 100644 --- a/ui/actions/overview/threat-map.adapter.ts +++ b/ui/actions/overview/regions/threat-map.adapter.ts @@ -1,6 +1,6 @@ import { getProviderDisplayName } from "@/types/providers"; -import { RegionsOverviewResponse } from "./types"; +import { RegionsOverviewResponse } from "../types"; export interface ThreatMapLocation { id: string; diff --git a/ui/actions/overview/services/index.ts b/ui/actions/overview/services/index.ts new file mode 100644 index 0000000000..b2221a94a8 --- /dev/null +++ b/ui/actions/overview/services/index.ts @@ -0,0 +1 @@ +export * from "./services"; diff --git a/ui/actions/overview/services/services.ts b/ui/actions/overview/services/services.ts new file mode 100644 index 0000000000..78d583c2c7 --- /dev/null +++ b/ui/actions/overview/services/services.ts @@ -0,0 +1,33 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { ServicesOverviewResponse } from "../types"; + +export const getServicesOverview = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/services`); + + 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; + } +}; diff --git a/ui/actions/overview/severity-trends.ts b/ui/actions/overview/severity-trends.ts deleted file mode 100644 index 59000c3f3a..0000000000 --- a/ui/actions/overview/severity-trends.ts +++ /dev/null @@ -1,186 +0,0 @@ -"use server"; - -const TIME_RANGE_OPTIONS = { - ONE_DAY: { value: "1D", days: 1 }, - FIVE_DAYS: { value: "5D", days: 5 }, - ONE_WEEK: { value: "1W", days: 7 }, - ONE_MONTH: { value: "1M", days: 30 }, -} as const; - -type TimeRange = - (typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS]["value"]; - -const getFindingsSeverityTrends = async ({ - filters = {}, -}: { - filters?: Record; -} = {}) => { - // TODO: Replace with actual API call when endpoint is available - // const headers = await getAuthHeaders({ contentType: false }); - // const url = new URL(`${apiBaseUrl}/findings/severity/time-series`); - // Object.entries(filters).forEach(([key, value]) => { - // if (value) url.searchParams.append(key, String(value)); - // }); - // const response = await fetch(url.toString(), { headers }); - // return handleApiResponse(response); - - // Extract date range from filters to simulate different data based on selection - const startDateStr = filters["filter[inserted_at__gte]"] as - | string - | undefined; - const endDateStr = filters["filter[inserted_at__lte]"] as string | undefined; - - // Generate mock data based on the date range - let mockData; - - if (startDateStr && endDateStr) { - const startDate = new Date(startDateStr); - const endDate = new Date(endDateStr); - const daysDiff = Math.ceil( - (endDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000), - ); - - // Generate data points for each day in the range - const dataPoints = []; - for (let i = 0; i <= daysDiff; i++) { - const currentDate = new Date(startDate); - currentDate.setDate(currentDate.getDate() + i); - const dateStr = currentDate.toISOString().split("T")[0]; - - // Vary the data based on the day for visual difference - const dayOffset = i; - dataPoints.push({ - type: "severity-time-series", - id: dateStr, - date: `${dateStr}T00:00:00Z`, - informational: Math.max(0, 380 + dayOffset * 15), - low: Math.max(0, 720 + dayOffset * 20), - medium: Math.max(0, 550 + dayOffset * 10), - high: Math.max(0, 1000 - dayOffset * 5), - critical: Math.max(0, 1200 - dayOffset * 30), - muted: Math.max(0, 500 - dayOffset * 25), - }); - } - - mockData = { - data: dataPoints, - links: { - self: `https://api.prowler.com/api/v1/findings/severity/time-series?start=${startDateStr}&end=${endDateStr}`, - }, - meta: { - date_range: `${startDateStr} to ${endDateStr}`, - days: daysDiff, - granularity: "daily", - timezone: "UTC", - }, - }; - } else { - // Default 5-day data if no date range provided - mockData = { - data: [ - { - type: "severity-time-series", - id: "2025-10-26", - date: "2025-10-26T00:00:00Z", - informational: 420, - low: 950, - medium: 720, - high: 1150, - critical: 1350, - muted: 600, - }, - { - type: "severity-time-series", - id: "2025-10-27", - date: "2025-10-27T00:00:00Z", - informational: 450, - low: 1100, - medium: 850, - high: 1300, - critical: 1500, - muted: 700, - }, - { - type: "severity-time-series", - id: "2025-10-28", - date: "2025-10-28T00:00:00Z", - informational: 400, - low: 850, - medium: 650, - high: 1200, - critical: 2000, - muted: 750, - }, - { - type: "severity-time-series", - id: "2025-10-29", - date: "2025-10-29T00:00:00Z", - informational: 380, - low: 720, - medium: 550, - high: 1000, - critical: 1200, - muted: 500, - }, - { - type: "severity-time-series", - id: "2025-11-10", - date: "2025-11-10T00:00:00Z", - informational: 500, - low: 750, - medium: 350, - high: 1000, - critical: 550, - muted: 100, - }, - ], - links: { - self: "https://api.prowler.com/api/v1/findings/severity/time-series?range=5D", - }, - meta: { - time_range: "5D", - granularity: "daily", - timezone: "UTC", - }, - }; - } - - return mockData; -}; - -export const getSeverityTrendsByTimeRange = async ({ - timeRange, - filters = {}, -}: { - timeRange: TimeRange; - filters?: Record; -}) => { - // Find the days value from TIME_RANGE_OPTIONS - const timeRangeConfig = Object.values(TIME_RANGE_OPTIONS).find( - (option) => option.value === timeRange, - ); - - if (!timeRangeConfig) { - throw new Error(`Invalid time range: ${timeRange}`); - } - - const endDate = new Date(); - const startDate = new Date( - endDate.getTime() - timeRangeConfig.days * 24 * 60 * 60 * 1000, - ); - - // Format dates as ISO strings for API - const startDateStr = startDate.toISOString().split("T")[0]; - const endDateStr = endDate.toISOString().split("T")[0]; - - // Add date filters to the request - const dateFilters = { - ...filters, - "filter[inserted_at__gte]": startDateStr, - "filter[inserted_at__lte]": endDateStr, - }; - - return getFindingsSeverityTrends({ filters: dateFilters }); -}; - -export { getFindingsSeverityTrends }; diff --git a/ui/actions/overview/severity-trends/index.ts b/ui/actions/overview/severity-trends/index.ts new file mode 100644 index 0000000000..bd84c67d6f --- /dev/null +++ b/ui/actions/overview/severity-trends/index.ts @@ -0,0 +1,2 @@ +export * from "./severity-trends"; +export * from "./severity-trends.adapter"; diff --git a/ui/actions/overview/severity-trends/severity-trends.adapter.ts b/ui/actions/overview/severity-trends/severity-trends.adapter.ts new file mode 100644 index 0000000000..44a6d3dca8 --- /dev/null +++ b/ui/actions/overview/severity-trends/severity-trends.adapter.ts @@ -0,0 +1,48 @@ +import { LineDataPoint } from "@/components/graphs/types"; + +import { + AdaptedSeverityTrendsResponse, + FindingsSeverityOverTimeResponse, +} from "../types"; + +export type { AdaptedSeverityTrendsResponse, FindingsSeverityOverTimeResponse }; + +/** + * Adapts the API findings severity over time response to the format expected by LineChart. + * Transforms API response with nested attributes into flat LineDataPoint objects. + * + * @param response - The raw API response from /overviews/findings_severity_over_time + * @returns Adapted response with LineDataPoint array ready for the chart + */ +export function adaptSeverityTrendsResponse( + response: FindingsSeverityOverTimeResponse, +): AdaptedSeverityTrendsResponse { + const adaptedData: LineDataPoint[] = response.data.map( + ({ + id, + attributes: { + informational, + low, + medium, + high, + critical, + muted, + scan_ids, + }, + }) => ({ + date: id, + informational, + low, + medium, + high, + critical, + muted, + scan_ids, + }), + ); + + return { + data: adaptedData, + meta: response.meta, + }; +} diff --git a/ui/actions/overview/severity-trends/severity-trends.ts b/ui/actions/overview/severity-trends/severity-trends.ts new file mode 100644 index 0000000000..062cb4eb81 --- /dev/null +++ b/ui/actions/overview/severity-trends/severity-trends.ts @@ -0,0 +1,99 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +import { + AdaptedSeverityTrendsResponse, + FindingsSeverityOverTimeResponse, +} from "../types"; +import { adaptSeverityTrendsResponse } from "./severity-trends.adapter"; + +const TIME_RANGE_VALUES = { + FIVE_DAYS: "5D", + ONE_WEEK: "1W", + ONE_MONTH: "1M", +} as const; + +type TimeRange = (typeof TIME_RANGE_VALUES)[keyof typeof TIME_RANGE_VALUES]; + +const TIME_RANGE_DAYS: Record = { + "5D": 5, + "1W": 7, + "1M": 30, +}; + +export type SeverityTrendsResult = + | { status: "success"; data: AdaptedSeverityTrendsResponse } + | { status: "empty" } + | { status: "error" }; + +const getFindingsSeverityTrends = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/findings_severity_over_time`); + + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers, + }); + + const apiResponse: FindingsSeverityOverTimeResponse | undefined = + await handleApiResponse(response); + + if (!apiResponse?.data || !Array.isArray(apiResponse.data)) { + return { status: "empty" }; + } + + if (apiResponse.data.length === 0) { + return { status: "empty" }; + } + + return { + status: "success", + data: adaptSeverityTrendsResponse(apiResponse), + }; + } catch (error) { + console.error("Error fetching findings severity trends:", error); + return { status: "error" }; + } +}; + +export const getSeverityTrendsByTimeRange = async ({ + timeRange, + filters = {}, +}: { + timeRange: TimeRange; + filters?: Record; +}): Promise => { + const days = TIME_RANGE_DAYS[timeRange]; + + if (!days) { + console.error("Invalid time range provided"); + return { status: "error" }; + } + + const endDate = new Date(); + const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000); + + const dateFrom = startDate.toISOString().split("T")[0]; + + const dateFilters = { + ...filters, + date_from: dateFrom, + }; + + return getFindingsSeverityTrends({ filters: dateFilters }); +}; + +export { getFindingsSeverityTrends }; diff --git a/ui/actions/overview/threat-score/index.ts b/ui/actions/overview/threat-score/index.ts new file mode 100644 index 0000000000..025ce3e1b9 --- /dev/null +++ b/ui/actions/overview/threat-score/index.ts @@ -0,0 +1 @@ +export * from "./threat-score"; diff --git a/ui/actions/overview/threat-score/threat-score.ts b/ui/actions/overview/threat-score/threat-score.ts new file mode 100644 index 0000000000..3a78d1ecda --- /dev/null +++ b/ui/actions/overview/threat-score/threat-score.ts @@ -0,0 +1,32 @@ +"use server"; + +import { apiBaseUrl, getAuthHeaders } from "@/lib"; +import { handleApiResponse } from "@/lib/server-actions-helper"; + +export const getThreatScore = async ({ + filters = {}, +}: { + filters?: Record; +} = {}) => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/threatscore`); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers, + }); + + return handleApiResponse(response); + } catch (error) { + console.error("Error fetching threat score:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/types/index.ts b/ui/actions/overview/types/index.ts index d3b9248bc7..074eb6abd9 100644 --- a/ui/actions/overview/types/index.ts +++ b/ui/actions/overview/types/index.ts @@ -4,4 +4,5 @@ export * from "./findings-severity"; export * from "./providers"; export * from "./regions"; export * from "./services"; +export * from "./severity-trends"; export * from "./threat-score"; diff --git a/ui/actions/overview/types/severity-trends.ts b/ui/actions/overview/types/severity-trends.ts new file mode 100644 index 0000000000..4778031abe --- /dev/null +++ b/ui/actions/overview/types/severity-trends.ts @@ -0,0 +1,33 @@ +import { LineDataPoint } from "@/components/graphs/types"; + +// API Response Types (what comes from the backend) +export interface FindingsSeverityOverTimeAttributes { + critical: number; + high: number; + medium: number; + low: number; + informational: number; + muted: number; + scan_ids: string[]; +} + +export interface FindingsSeverityOverTimeItem { + type: "findings-severity-over-time"; + id: string; + attributes: FindingsSeverityOverTimeAttributes; +} + +export interface FindingsSeverityOverTimeMeta { + version: string; +} + +export interface FindingsSeverityOverTimeResponse { + data: FindingsSeverityOverTimeItem[]; + meta: FindingsSeverityOverTimeMeta; +} + +// Adapted Types (what the UI components expect) +export interface AdaptedSeverityTrendsResponse { + data: LineDataPoint[]; + meta: FindingsSeverityOverTimeMeta; +} diff --git a/ui/actions/task/index.ts b/ui/actions/task/index.ts index 078ba8fc62..f5752264ce 100644 --- a/ui/actions/task/index.ts +++ b/ui/actions/task/index.ts @@ -1 +1,2 @@ +export * from "./poll"; export * from "./tasks"; diff --git a/ui/actions/users/index.ts b/ui/actions/users/index.ts new file mode 100644 index 0000000000..7aefc80fd2 --- /dev/null +++ b/ui/actions/users/index.ts @@ -0,0 +1,2 @@ +export * from "./tenants"; +export * from "./users"; diff --git a/ui/app/(prowler)/_new-overview/components/accounts-selector.tsx b/ui/app/(prowler)/_new-overview/_components/accounts-selector.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/accounts-selector.tsx rename to ui/app/(prowler)/_new-overview/_components/accounts-selector.tsx diff --git a/ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx b/ui/app/(prowler)/_new-overview/_components/provider-type-selector.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/provider-type-selector.tsx rename to ui/app/(prowler)/_new-overview/_components/provider-type-selector.tsx diff --git a/ui/app/(prowler)/_new-overview/lib/filter-params.ts b/ui/app/(prowler)/_new-overview/_lib/filter-params.ts similarity index 100% rename from ui/app/(prowler)/_new-overview/lib/filter-params.ts rename to ui/app/(prowler)/_new-overview/_lib/filter-params.ts diff --git a/ui/app/(prowler)/_new-overview/_types.ts b/ui/app/(prowler)/_new-overview/_types.ts new file mode 100644 index 0000000000..9dc7fa2c97 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/_types.ts @@ -0,0 +1,9 @@ +import { SearchParamsProps } from "@/types"; + +/** + * Common props interface for SSR components that receive search params + * from the page component for filter handling. + */ +export interface SSRComponentProps { + searchParams: SearchParamsProps | undefined | null; +} diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-card-item.tsx b/ui/app/(prowler)/_new-overview/attack-surface/_components/attack-surface-card-item.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-card-item.tsx rename to ui/app/(prowler)/_new-overview/attack-surface/_components/attack-surface-card-item.tsx diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx b/ui/app/(prowler)/_new-overview/attack-surface/_components/attack-surface.tsx similarity index 93% rename from ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx rename to ui/app/(prowler)/_new-overview/attack-surface/_components/attack-surface.tsx index 3b0c990e1e..5f0bea54b5 100644 --- a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx +++ b/ui/app/(prowler)/_new-overview/attack-surface/_components/attack-surface.tsx @@ -1,4 +1,4 @@ -import { AttackSurfaceItem } from "@/actions/overview/attack-surface.adapter"; +import { AttackSurfaceItem } from "@/actions/overview"; import { Card, CardContent, CardTitle } from "@/components/shadcn"; import { AttackSurfaceCardItem } from "./attack-surface-card-item"; diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-skeleton.tsx b/ui/app/(prowler)/_new-overview/attack-surface/attack-surface-skeleton.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-skeleton.tsx rename to ui/app/(prowler)/_new-overview/attack-surface/attack-surface-skeleton.tsx diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx b/ui/app/(prowler)/_new-overview/attack-surface/attack-surface.ssr.tsx similarity index 54% rename from ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx rename to ui/app/(prowler)/_new-overview/attack-surface/attack-surface.ssr.tsx index e20dc483bb..7f5b0b0f5f 100644 --- a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/attack-surface/attack-surface.ssr.tsx @@ -2,16 +2,12 @@ import { adaptAttackSurfaceOverview, getAttackSurfaceOverview, } from "@/actions/overview"; -import { SearchParamsProps } from "@/types"; -import { pickFilterParams } from "../../lib/filter-params"; -import { AttackSurface } from "./attack-surface"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { AttackSurface } from "./_components/attack-surface"; -export const AttackSurfaceSSR = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +export const AttackSurfaceSSR = async ({ searchParams }: SSRComponentProps) => { const filters = pickFilterParams(searchParams); const response = await getAttackSurfaceOverview({ filters }); diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/index.ts b/ui/app/(prowler)/_new-overview/attack-surface/index.ts similarity index 51% rename from ui/app/(prowler)/_new-overview/components/attack-surface/index.ts rename to ui/app/(prowler)/_new-overview/attack-surface/index.ts index 8ab335b4ba..4f2413e9a9 100644 --- a/ui/app/(prowler)/_new-overview/components/attack-surface/index.ts +++ b/ui/app/(prowler)/_new-overview/attack-surface/index.ts @@ -1,4 +1,2 @@ -export { AttackSurface } from "./attack-surface"; export { AttackSurfaceSSR } from "./attack-surface.ssr"; -export { AttackSurfaceCardItem } from "./attack-surface-card-item"; export { AttackSurfaceSkeleton } from "./attack-surface-skeleton"; diff --git a/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx b/ui/app/(prowler)/_new-overview/check-findings/check-findings.ssr.tsx similarity index 67% rename from ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx rename to ui/app/(prowler)/_new-overview/check-findings/check-findings.ssr.tsx index 0e0b2de402..b194452e81 100644 --- a/ui/app/(prowler)/_new-overview/components/check-findings/check-findings.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/check-findings/check-findings.ssr.tsx @@ -1,14 +1,10 @@ -import { getFindingsByStatus } from "@/actions/overview/overview"; -import { SearchParamsProps } from "@/types"; +import { getFindingsByStatus } from "@/actions/overview"; -import { pickFilterParams } from "../../lib/filter-params"; -import { StatusChart } from "../status-chart/status-chart"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { StatusChart } from "../status-chart/_components/status-chart"; -export const CheckFindingsSSR = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +export const CheckFindingsSSR = async ({ searchParams }: SSRComponentProps) => { const filters = pickFilterParams(searchParams); const findingsByStatus = await getFindingsByStatus({ filters }); diff --git a/ui/app/(prowler)/_new-overview/components/check-findings/index.ts b/ui/app/(prowler)/_new-overview/check-findings/index.ts similarity index 100% rename from ui/app/(prowler)/_new-overview/components/check-findings/index.ts rename to ui/app/(prowler)/_new-overview/check-findings/index.ts diff --git a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time-detail.ssr.tsx b/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time-detail.ssr.tsx deleted file mode 100644 index cf0bc76309..0000000000 --- a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time-detail.ssr.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends"; -import { SearchParamsProps } from "@/types"; - -import { pickFilterParams } from "../../lib/filter-params"; -import { FindingSeverityOverTime } from "./finding-severity-over-time"; - -export const FindingSeverityOverTimeDetailSSR = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { - const filters = pickFilterParams(searchParams); - - const severityTrends = await getFindingsSeverityTrends({ filters }); - - if ( - !severityTrends || - !severityTrends.data || - severityTrends.data.length === 0 - ) { - return ( -
-

- Failed to load severity trends data -

-
- ); - } - - return ( -
-

- Finding Severity Over Time -

- -
- ); -}; diff --git a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.ssr.tsx b/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.ssr.tsx deleted file mode 100644 index 0ce5562607..0000000000 --- a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.ssr.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; -import { SearchParamsProps } from "@/types"; - -import { pickFilterParams } from "../../lib/filter-params"; -import { - FindingSeverityOverTime, - FindingSeverityOverTimeSkeleton, -} from "./finding-severity-over-time"; - -export { FindingSeverityOverTimeSkeleton }; - -export const FindingSeverityOverTimeSSR = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { - const filters = pickFilterParams(searchParams); - - const severityTrends = await getFindingsSeverityTrends({ filters }); - - if ( - !severityTrends || - !severityTrends.data || - severityTrends.data.length === 0 - ) { - return ( -
-

- Failed to load severity trends data -

-
- ); - } - - return ( - - -
- Finding Severity Over Time -
-
- - - - -
- ); -}; diff --git a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.tsx b/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.tsx deleted file mode 100644 index b2bd8b6add..0000000000 --- a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/finding-severity-over-time.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends"; -import { LineChart } from "@/components/graphs/line-chart"; -import { LineConfig, LineDataPoint } from "@/components/graphs/types"; -import { Skeleton } from "@/components/shadcn"; -import { SEVERITY_LINE_CONFIGS } from "@/types/severities"; - -import { type TimeRange, TimeRangeSelector } from "./time-range-selector"; - -interface SeverityDataPoint { - type: string; - id: string; - date: string; - informational: number; - low: number; - medium: number; - high: number; - critical: number; - muted?: number; -} - -interface FindingSeverityOverTimeProps { - data: SeverityDataPoint[]; -} - -export const FindingSeverityOverTime = ({ - data: initialData, -}: FindingSeverityOverTimeProps) => { - const [timeRange, setTimeRange] = useState("5D"); - const [data, setData] = useState(initialData); - const [isLoading, setIsLoading] = useState(false); - - const handleTimeRangeChange = async (newRange: TimeRange) => { - setTimeRange(newRange); - setIsLoading(true); - - try { - const response = await getSeverityTrendsByTimeRange({ - timeRange: newRange, - }); - - if (response?.data) { - setData(response.data); - } - } catch (error) { - console.error("Error fetching severity trends"); - } finally { - setIsLoading(false); - } - }; - - // Transform API data into LineDataPoint format - const chartData: LineDataPoint[] = data.map((item) => { - const date = new Date(item.date); - const formattedDate = date.toLocaleDateString("en-US", { - month: "2-digit", - day: "2-digit", - }); - - return { - date: formattedDate, - informational: item.informational, - low: item.low, - medium: item.medium, - high: item.high, - critical: item.critical, - ...(item.muted && { muted: item.muted }), - }; - }); - - // Build line configurations from shared severity configs - const lines: LineConfig[] = [...SEVERITY_LINE_CONFIGS]; - - // Only add muted line if data contains it (CSS var for Recharts inline styles) - if (data.some((item) => item.muted !== undefined)) { - lines.push({ - dataKey: "muted", - color: "var(--color-bg-data-muted)", - label: "Muted", - }); - } - - return ( - <> -
- -
-
- -
- - ); -}; - -export function FindingSeverityOverTimeSkeleton() { - return ( - <> -
-
- {Array.from({ length: 4 }).map((_, index) => ( - - ))} -
-
- - - ); -} diff --git a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/index.ts b/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/index.ts deleted file mode 100644 index c77490aab5..0000000000 --- a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { FindingSeverityOverTime } from "./finding-severity-over-time"; -export { FindingSeverityOverTimeSSR } from "./finding-severity-over-time.ssr"; -export { TimeRangeSelector } from "./time-range-selector"; diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/index.ts b/ui/app/(prowler)/_new-overview/components/risk-severity-chart/index.ts deleted file mode 100644 index 513cf723a8..0000000000 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - RiskSeverityChart, - RiskSeverityChartSkeleton, -} from "./risk-severity-chart"; -export { RiskSeverityChartSSR } from "./risk-severity-chart.ssr"; diff --git a/ui/app/(prowler)/_new-overview/components/status-chart/index.ts b/ui/app/(prowler)/_new-overview/components/status-chart/index.ts deleted file mode 100644 index 8c546469ce..0000000000 --- a/ui/app/(prowler)/_new-overview/components/status-chart/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { StatusChart, StatusChartSkeleton } from "./status-chart"; diff --git a/ui/app/(prowler)/_new-overview/components/threat-score/index.ts b/ui/app/(prowler)/_new-overview/components/threat-score/index.ts deleted file mode 100644 index 48f386d947..0000000000 --- a/ui/app/(prowler)/_new-overview/components/threat-score/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ThreatScore, ThreatScoreSkeleton } from "./threat-score"; -export { ThreatScoreSSR } from "./threat-score.ssr"; diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/index.ts b/ui/app/(prowler)/_new-overview/components/watchlist/index.ts deleted file mode 100644 index 7e677311d2..0000000000 --- a/ui/app/(prowler)/_new-overview/components/watchlist/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type { ComplianceData } from "./compliance-watchlist"; -export { ComplianceWatchlist } from "./compliance-watchlist"; -export { ComplianceWatchlistSSR } from "./compliance-watchlist.ssr"; -export { ServiceWatchlist } from "./service-watchlist"; -export { ServiceWatchlistSSR } from "./service-watchlist.ssr"; -export { SortToggleButton } from "./sort-toggle-button"; -export * from "./watchlist-card"; diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/_components/graphs-tabs-client.tsx similarity index 95% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/_components/graphs-tabs-client.tsx index 0dfd999ea4..1e2bc3f1f2 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-client.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/_components/graphs-tabs-client.tsx @@ -4,7 +4,7 @@ import { useRef, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/shadcn"; -import { GRAPH_TABS, type TabId } from "./graphs-tabs-config"; +import { GRAPH_TABS, type TabId } from "../_config/graphs-tabs-config"; interface GraphsTabsClientProps { tabsContent: Record; diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts b/ui/app/(prowler)/_new-overview/graphs-tabs/_config/graphs-tabs-config.ts similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-config.ts rename to ui/app/(prowler)/_new-overview/graphs-tabs/_config/graphs-tabs-config.ts diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/findings-view/findings-view.ssr.tsx similarity index 78% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/findings-view/findings-view.ssr.tsx index 32cfdd6ec8..7dcbac50e9 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/findings-view.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/findings-view/findings-view.ssr.tsx @@ -3,24 +3,15 @@ import { Spacer } from "@heroui/spacer"; import { getLatestFindings } from "@/actions/findings/findings"; +import { LighthouseBanner } from "@/components/lighthouse/banner"; import { LinkToFindings } from "@/components/overview"; import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date"; import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib/helper"; +import { mapProviderFiltersForFindingsObject } from "@/lib/provider-helpers"; import { FindingProps, SearchParamsProps } from "@/types"; -import { LighthouseBanner } from "../../../../../../components/lighthouse/banner"; - -const FILTER_PREFIX = "filter["; - -function pickFilterParams( - params: SearchParamsProps | undefined | null, -): Record { - if (!params) return {}; - return Object.fromEntries( - Object.entries(params).filter(([key]) => key.startsWith(FILTER_PREFIX)), - ); -} +import { pickFilterParams } from "../../_lib/filter-params"; interface FindingsViewSSRProps { searchParams: SearchParamsProps; @@ -36,15 +27,7 @@ export async function FindingsViewSSR({ searchParams }: FindingsViewSSRProps) { }; const filters = pickFilterParams(searchParams); - - // Map provider_id__in to provider__in for findings API - const mappedFilters = { ...filters }; - if (mappedFilters["filter[provider_id__in]"]) { - mappedFilters["filter[provider__in]"] = - mappedFilters["filter[provider_id__in]"]; - delete mappedFilters["filter[provider_id__in]"]; - } - + const mappedFilters = mapProviderFiltersForFindingsObject(filters); const combinedFilters = { ...defaultFilters, ...mappedFilters }; const findingsData = await getLatestFindings({ diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/index.ts b/ui/app/(prowler)/_new-overview/graphs-tabs/findings-view/index.ts similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/findings-view/index.ts rename to ui/app/(prowler)/_new-overview/graphs-tabs/findings-view/index.ts diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-wrapper.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/graphs-tabs-wrapper.tsx similarity index 93% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-wrapper.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/graphs-tabs-wrapper.tsx index 5b9d4ff870..d3862809a6 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/graphs-tabs-wrapper.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/graphs-tabs-wrapper.tsx @@ -3,9 +3,9 @@ import { Suspense } from "react"; import { SearchParamsProps } from "@/types"; +import { GraphsTabsClient } from "./_components/graphs-tabs-client"; +import { GRAPH_TABS, type TabId } from "./_config/graphs-tabs-config"; import { FindingsViewSSR } from "./findings-view"; -import { GraphsTabsClient } from "./graphs-tabs-client"; -import { GRAPH_TABS, type TabId } from "./graphs-tabs-config"; import { RiskPipelineViewSSR } from "./risk-pipeline-view/risk-pipeline-view.ssr"; import { ThreatMapViewSSR } from "./threat-map-view/threat-map-view.ssr"; // TODO: Uncomment when ready to enable other tabs diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/index.ts b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/index.ts similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/index.ts rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/index.ts diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view-skeleton.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/risk-pipeline-view-skeleton.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view-skeleton.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/risk-pipeline-view-skeleton.tsx diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx similarity index 98% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx index dd1a3a7138..509c92fa2c 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-pipeline-view/risk-pipeline-view.ssr.tsx @@ -7,7 +7,7 @@ import { getProviders } from "@/actions/providers"; import { SankeyChart } from "@/components/graphs/sankey-chart"; import { SearchParamsProps } from "@/types"; -import { pickFilterParams } from "../../../lib/filter-params"; +import { pickFilterParams } from "../../_lib/filter-params"; export async function RiskPipelineViewSSR({ searchParams, diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-plot/risk-plot-client.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-plot/risk-plot-client.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-plot/risk-plot-view.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-plot/risk-plot-view.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-radar-view/risk-radar-view-client.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-radar-view/risk-radar-view-client.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-radar-view/risk-radar-view-client.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-radar-view/risk-radar-view-client.tsx diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-radar-view/risk-radar-view.ssr.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-radar-view/risk-radar-view.ssr.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/risk-radar-view/risk-radar-view.ssr.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/risk-radar-view/risk-radar-view.ssr.tsx diff --git a/ui/app/(prowler)/_new-overview/components/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx similarity index 93% rename from ui/app/(prowler)/_new-overview/components/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx rename to ui/app/(prowler)/_new-overview/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx index 5e62d1bedf..7e25a1a650 100644 --- a/ui/app/(prowler)/_new-overview/components/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/threat-map-view/threat-map-view.ssr.tsx @@ -5,7 +5,7 @@ import { import { ThreatMap } from "@/components/graphs/threat-map"; import { SearchParamsProps } from "@/types"; -import { pickFilterParams } from "../../../lib/filter-params"; +import { pickFilterParams } from "../../_lib/filter-params"; export async function ThreatMapViewSSR({ searchParams, diff --git a/ui/app/(prowler)/_new-overview/page.tsx b/ui/app/(prowler)/_new-overview/page.tsx index ac061defb7..a9886782c0 100644 --- a/ui/app/(prowler)/_new-overview/page.tsx +++ b/ui/app/(prowler)/_new-overview/page.tsx @@ -4,23 +4,23 @@ import { getProviders } from "@/actions/providers"; import { ContentLayout } from "@/components/ui"; import { SearchParamsProps } from "@/types"; -import { AccountsSelector } from "./components/accounts-selector"; -import { CheckFindingsSSR } from "./components/check-findings"; +import { AccountsSelector } from "./_components/accounts-selector"; +import { ProviderTypeSelector } from "./_components/provider-type-selector"; +import { CheckFindingsSSR } from "./check-findings"; +import { GraphsTabsWrapper } from "./graphs-tabs/graphs-tabs-wrapper"; +import { RiskSeverityChartSkeleton } from "./risk-severity"; +import { RiskSeverityChartSSR } from "./risk-severity/risk-severity-chart.ssr"; import { FindingSeverityOverTimeSkeleton, FindingSeverityOverTimeSSR, -} from "./components/finding-severity-over-time/finding-severity-over-time.ssr"; -import { GraphsTabsWrapper } from "./components/graphs-tabs/graphs-tabs-wrapper"; -import { ProviderTypeSelector } from "./components/provider-type-selector"; -import { RiskSeverityChartSkeleton } from "./components/risk-severity-chart"; -import { RiskSeverityChartSSR } from "./components/risk-severity-chart/risk-severity-chart.ssr"; -import { StatusChartSkeleton } from "./components/status-chart"; -import { ThreatScoreSkeleton, ThreatScoreSSR } from "./components/threat-score"; +} from "./severity-over-time/finding-severity-over-time.ssr"; +import { StatusChartSkeleton } from "./status-chart"; +import { ThreatScoreSkeleton, ThreatScoreSSR } from "./threat-score"; import { ComplianceWatchlistSSR, ServiceWatchlistSSR, WatchlistCardSkeleton, -} from "./components/watchlist"; +} from "./watchlist"; export default async function NewOverviewPage({ searchParams, diff --git a/ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.skeleton.tsx b/ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.skeleton.tsx new file mode 100644 index 0000000000..8afb8a2df1 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.skeleton.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent, CardHeader, Skeleton } from "@/components/shadcn"; + +export function RiskSeverityChartSkeleton() { + return ( + + + + + + +
+ {/* 5 horizontal bar skeletons */} + {Array.from({ length: 5 }).map((_, index) => ( +
+ + +
+ ))} +
+
+
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx b/ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.tsx similarity index 73% rename from ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx rename to ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.tsx index a9f0c6d0a5..e718168698 100644 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.tsx +++ b/ui/app/(prowler)/_new-overview/risk-severity/_components/risk-severity-chart.tsx @@ -4,13 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { BarDataPoint } from "@/components/graphs/types"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - Skeleton, -} from "@/components/shadcn"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; @@ -100,28 +94,3 @@ export const RiskSeverityChart = ({ ); }; - -export function RiskSeverityChartSkeleton() { - return ( - - - - - - -
- {/* 5 horizontal bar skeletons */} - {Array.from({ length: 5 }).map((_, index) => ( -
- - -
- ))} -
-
-
- ); -} diff --git a/ui/app/(prowler)/_new-overview/risk-severity/index.ts b/ui/app/(prowler)/_new-overview/risk-severity/index.ts new file mode 100644 index 0000000000..b59785f2f2 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/risk-severity/index.ts @@ -0,0 +1,3 @@ +export { RiskSeverityChart } from "./_components/risk-severity-chart"; +export { RiskSeverityChartSkeleton } from "./_components/risk-severity-chart.skeleton"; +export { RiskSeverityChartSSR } from "./risk-severity-chart.ssr"; diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx b/ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart-detail.ssr.tsx similarity index 73% rename from ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx rename to ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart-detail.ssr.tsx index 93cbe30fbd..7091232a80 100644 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart-detail.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart-detail.ssr.tsx @@ -1,14 +1,12 @@ -import { getFindingsBySeverity } from "@/actions/overview/overview"; -import { SearchParamsProps } from "@/types"; +import { getFindingsBySeverity } from "@/actions/overview"; -import { pickFilterParams } from "../../lib/filter-params"; -import { RiskSeverityChart } from "./risk-severity-chart"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { RiskSeverityChart } from "./_components/risk-severity-chart"; export const RiskSeverityChartDetailSSR = async ({ searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +}: SSRComponentProps) => { const filters = pickFilterParams(searchParams); // Filter by FAIL findings filters["filter[status]"] = "FAIL"; diff --git a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.ssr.tsx b/ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart.ssr.tsx similarity index 59% rename from ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.ssr.tsx rename to ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart.ssr.tsx index f68394f59d..16c4b75cbb 100644 --- a/ui/app/(prowler)/_new-overview/components/risk-severity-chart/risk-severity-chart.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/risk-severity/risk-severity-chart.ssr.tsx @@ -1,13 +1,10 @@ -import { SearchParamsProps } from "@/types"; - -import { pickFilterParams } from "../../lib/filter-params"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; import { RiskSeverityChartDetailSSR } from "./risk-severity-chart-detail.ssr"; export const RiskSeverityChartSSR = async ({ searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +}: SSRComponentProps) => { const filters = pickFilterParams(searchParams); return ; diff --git a/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.skeleton.tsx b/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.skeleton.tsx new file mode 100644 index 0000000000..39a6cf7ac2 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.skeleton.tsx @@ -0,0 +1,16 @@ +import { Skeleton } from "@/components/shadcn"; + +export function FindingSeverityOverTimeSkeleton() { + return ( +
+
+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+ +
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.tsx b/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.tsx new file mode 100644 index 0000000000..79ed2c5b40 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/severity-over-time/_components/finding-severity-over-time.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; + +import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends"; +import { LineChart } from "@/components/graphs/line-chart"; +import { LineConfig, LineDataPoint } from "@/components/graphs/types"; +import { + MUTED_COLOR, + SEVERITY_LEVELS, + SEVERITY_LINE_CONFIGS, + SeverityLevel, +} from "@/types/severities"; + +import { type TimeRange, TimeRangeSelector } from "./time-range-selector"; + +interface FindingSeverityOverTimeProps { + data: LineDataPoint[]; +} + +export const FindingSeverityOverTime = ({ + data: initialData, +}: FindingSeverityOverTimeProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const [timeRange, setTimeRange] = useState("5D"); + const [data, setData] = useState(initialData); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handlePointClick = ({ + point, + dataKey, + }: { + point: LineDataPoint; + dataKey?: string; + }) => { + const params = new URLSearchParams(); + params.set("filter[inserted_at]", point.date); + + // Add scan_ids filter + if ( + point.scan_ids && + Array.isArray(point.scan_ids) && + point.scan_ids.length > 0 + ) { + params.set("filter[scan__in]", point.scan_ids.join(",")); + } + + // Add severity filter if clicked on a specific severity line + if (dataKey && SEVERITY_LEVELS.includes(dataKey as SeverityLevel)) { + params.set("filter[severity__in]", dataKey); + } + + // Preserve provider filters from overview + const providerType = searchParams.get("filter[provider_type__in]"); + const providerId = searchParams.get("filter[provider_id__in]"); + + if (providerType) { + params.set("filter[provider_type__in]", providerType); + } + if (providerId) { + params.set("filter[provider__in]", providerId); + } + + router.push(`/findings?${params.toString()}`); + }; + + const handleTimeRangeChange = async (newRange: TimeRange) => { + setTimeRange(newRange); + setIsLoading(true); + setError(null); + + try { + const result = await getSeverityTrendsByTimeRange({ + timeRange: newRange, + }); + + if (result.status === "success") { + setData(result.data.data); + } else if (result.status === "empty") { + setData([]); + setError("No severity trends data available for this time range"); + } else { + setError("Failed to load severity trends. Please try again."); + } + } catch (err) { + console.error("Error fetching severity trends:", err); + setError("Failed to load severity trends. Please try again."); + } finally { + setIsLoading(false); + } + }; + + // Build line configurations from shared severity configs + const lines: LineConfig[] = [...SEVERITY_LINE_CONFIGS]; + + // Only add muted line if data contains it + if (data.some((item) => item.muted !== undefined)) { + lines.push({ + dataKey: "muted", + color: MUTED_COLOR, + label: "Muted", + }); + } + + // Calculate x-axis interval based on data length to show all labels without overlap + const getXAxisInterval = (): number => { + const dataLength = data.length; + if (dataLength <= 7) return 0; // Show all labels for 5D and 1W + return 0; // Show all labels for 1M too + }; + + return ( + <> +
+ +
+ {error ? ( +
+

{error}

+
+ ) : ( +
+ +
+ )} + + ); +}; diff --git a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/time-range-selector.tsx b/ui/app/(prowler)/_new-overview/severity-over-time/_components/time-range-selector.tsx similarity index 99% rename from ui/app/(prowler)/_new-overview/components/finding-severity-over-time/time-range-selector.tsx rename to ui/app/(prowler)/_new-overview/severity-over-time/_components/time-range-selector.tsx index 9c244fd2d3..60626fa4dc 100644 --- a/ui/app/(prowler)/_new-overview/components/finding-severity-over-time/time-range-selector.tsx +++ b/ui/app/(prowler)/_new-overview/severity-over-time/_components/time-range-selector.tsx @@ -3,7 +3,6 @@ import { cn } from "@/lib/utils"; const TIME_RANGE_OPTIONS = { - ONE_DAY: "1D", FIVE_DAYS: "5D", ONE_WEEK: "1W", ONE_MONTH: "1M", diff --git a/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time-detail.ssr.tsx b/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time-detail.ssr.tsx new file mode 100644 index 0000000000..a6d99a0109 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time-detail.ssr.tsx @@ -0,0 +1,35 @@ +import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends"; + +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { FindingSeverityOverTime } from "./_components/finding-severity-over-time"; + +const EmptyState = ({ message }: { message: string }) => ( +
+

{message}

+
+); + +export const FindingSeverityOverTimeDetailSSR = async ({ + searchParams, +}: SSRComponentProps) => { + const filters = pickFilterParams(searchParams); + const result = await getFindingsSeverityTrends({ filters }); + + if (result.status === "error") { + return ; + } + + if (result.status === "empty") { + return ; + } + + return ( +
+

+ Finding Severity Over Time +

+ +
+ ); +}; diff --git a/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time.ssr.tsx b/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time.ssr.tsx new file mode 100644 index 0000000000..29867ad321 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/severity-over-time/finding-severity-over-time.ssr.tsx @@ -0,0 +1,51 @@ +import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; + +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { FindingSeverityOverTime } from "./_components/finding-severity-over-time"; +import { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton"; + +export { FindingSeverityOverTimeSkeleton }; + +const EmptyState = ({ message }: { message: string }) => ( + + +
+ Finding Severity Over Time +
+
+ +

{message}

+
+
+); + +export const FindingSeverityOverTimeSSR = async ({ + searchParams, +}: SSRComponentProps) => { + const filters = pickFilterParams(searchParams); + const result = await getFindingsSeverityTrends({ filters }); + + if (result.status === "error") { + return ; + } + + if (result.status === "empty") { + return ; + } + + return ( + + +
+ Finding Severity Over Time +
+
+ + + + +
+ ); +}; diff --git a/ui/app/(prowler)/_new-overview/severity-over-time/index.ts b/ui/app/(prowler)/_new-overview/severity-over-time/index.ts new file mode 100644 index 0000000000..1bcf38dc88 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/severity-over-time/index.ts @@ -0,0 +1,4 @@ +export { FindingSeverityOverTime } from "./_components/finding-severity-over-time"; +export { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton"; +export { TimeRangeSelector } from "./_components/time-range-selector"; +export { FindingSeverityOverTimeSSR } from "./finding-severity-over-time.ssr"; diff --git a/ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.skeleton.tsx b/ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.skeleton.tsx new file mode 100644 index 0000000000..61fb836caf --- /dev/null +++ b/ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.skeleton.tsx @@ -0,0 +1,24 @@ +import { Card, CardContent, CardHeader, Skeleton } from "@/components/shadcn"; + +export function StatusChartSkeleton() { + return ( + + + + + + + {/* Circular skeleton for donut chart */} +
+ +
+ + {/* Bottom info box skeleton */} + +
+
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx b/ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.tsx similarity index 87% rename from ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx rename to ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.tsx index 44a9c209c0..4615410d80 100644 --- a/ui/app/(prowler)/_new-overview/components/status-chart/status-chart.tsx +++ b/ui/app/(prowler)/_new-overview/status-chart/_components/status-chart.tsx @@ -12,7 +12,6 @@ import { CardTitle, CardVariant, ResourceStatsCard, - Skeleton, } from "@/components/shadcn"; import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { calculatePercentage } from "@/lib/utils"; @@ -165,26 +164,3 @@ export const StatusChart = ({ ); }; - -export function StatusChartSkeleton() { - return ( - - - - - - - {/* Circular skeleton for donut chart */} -
- -
- - {/* Bottom info box skeleton */} - -
-
- ); -} diff --git a/ui/app/(prowler)/_new-overview/status-chart/index.ts b/ui/app/(prowler)/_new-overview/status-chart/index.ts new file mode 100644 index 0000000000..827ae77ef9 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/status-chart/index.ts @@ -0,0 +1,2 @@ +export { StatusChart } from "./_components/status-chart"; +export { StatusChartSkeleton } from "./_components/status-chart.skeleton"; diff --git a/ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.skeleton.tsx b/ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.skeleton.tsx new file mode 100644 index 0000000000..bc817e8c55 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.skeleton.tsx @@ -0,0 +1,24 @@ +import { Card, CardContent, CardHeader, Skeleton } from "@/components/shadcn"; + +export function ThreatScoreSkeleton() { + return ( + + + + + + + {/* Circular skeleton for radial chart */} +
+ +
+ + {/* Bottom info box skeleton */} + +
+
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/components/threat-score/threat-score.tsx b/ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.tsx similarity index 87% rename from ui/app/(prowler)/_new-overview/components/threat-score/threat-score.tsx rename to ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.tsx index e336f25402..657c7b5ae9 100644 --- a/ui/app/(prowler)/_new-overview/components/threat-score/threat-score.tsx +++ b/ui/app/(prowler)/_new-overview/threat-score/_components/threat-score.tsx @@ -7,13 +7,7 @@ import type { SectionScores, } from "@/actions/overview/types"; import { RadialChart } from "@/components/graphs/radial-chart"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - Skeleton, -} from "@/components/shadcn"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; // CSS variables are required here as they're passed to RadialChart component // which uses Recharts library that needs actual color values, not Tailwind classes @@ -119,7 +113,7 @@ export function ThreatScore({ return ( Prowler Threat Score @@ -208,26 +202,3 @@ export function ThreatScore({ ); } - -export function ThreatScoreSkeleton() { - return ( - - - - - - - {/* Circular skeleton for radial chart */} -
- -
- - {/* Bottom info box skeleton */} - -
-
- ); -} diff --git a/ui/app/(prowler)/_new-overview/threat-score/index.ts b/ui/app/(prowler)/_new-overview/threat-score/index.ts new file mode 100644 index 0000000000..04499ef30c --- /dev/null +++ b/ui/app/(prowler)/_new-overview/threat-score/index.ts @@ -0,0 +1,3 @@ +export { ThreatScore } from "./_components/threat-score"; +export { ThreatScoreSkeleton } from "./_components/threat-score.skeleton"; +export { ThreatScoreSSR } from "./threat-score.ssr"; diff --git a/ui/app/(prowler)/_new-overview/components/threat-score/threat-score.ssr.tsx b/ui/app/(prowler)/_new-overview/threat-score/threat-score.ssr.tsx similarity index 71% rename from ui/app/(prowler)/_new-overview/components/threat-score/threat-score.ssr.tsx rename to ui/app/(prowler)/_new-overview/threat-score/threat-score.ssr.tsx index 562f4f6f16..7f5364d147 100644 --- a/ui/app/(prowler)/_new-overview/components/threat-score/threat-score.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/threat-score/threat-score.ssr.tsx @@ -1,14 +1,10 @@ -import { getThreatScore } from "@/actions/overview/overview"; -import { SearchParamsProps } from "@/types"; +import { getThreatScore } from "@/actions/overview"; -import { pickFilterParams } from "../../lib/filter-params"; -import { ThreatScore } from "./threat-score"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { ThreatScore } from "./_components/threat-score"; -export const ThreatScoreSSR = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +export const ThreatScoreSSR = async ({ searchParams }: SSRComponentProps) => { const filters = pickFilterParams(searchParams); const threatScoreData = await getThreatScore({ filters }); diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.tsx b/ui/app/(prowler)/_new-overview/watchlist/_components/compliance-watchlist.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.tsx rename to ui/app/(prowler)/_new-overview/watchlist/_components/compliance-watchlist.tsx diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx b/ui/app/(prowler)/_new-overview/watchlist/_components/service-watchlist.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.tsx rename to ui/app/(prowler)/_new-overview/watchlist/_components/service-watchlist.tsx diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/sort-toggle-button.tsx b/ui/app/(prowler)/_new-overview/watchlist/_components/sort-toggle-button.tsx similarity index 100% rename from ui/app/(prowler)/_new-overview/components/watchlist/sort-toggle-button.tsx rename to ui/app/(prowler)/_new-overview/watchlist/_components/sort-toggle-button.tsx diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx b/ui/app/(prowler)/_new-overview/watchlist/_components/watchlist-card.tsx similarity index 89% rename from ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx rename to ui/app/(prowler)/_new-overview/watchlist/_components/watchlist-card.tsx index 5c9e71f58e..a74f4b493e 100644 --- a/ui/app/(prowler)/_new-overview/components/watchlist/watchlist-card.tsx +++ b/ui/app/(prowler)/_new-overview/watchlist/_components/watchlist-card.tsx @@ -9,7 +9,6 @@ import { CardFooter, CardTitle, } from "@/components/shadcn/card/card"; -import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; import { cn } from "@/lib/utils"; const SCORE_CONFIG = { @@ -192,23 +191,3 @@ export const WatchlistCard = ({ ); }; - -export function WatchlistCardSkeleton() { - return ( - - - - - - - {/* 6 skeleton rows */} - {Array.from({ length: 6 }).map((_, index) => ( -
- - -
- ))} -
-
- ); -} diff --git a/ui/app/(prowler)/_new-overview/watchlist/_components/watchlist.skeleton.tsx b/ui/app/(prowler)/_new-overview/watchlist/_components/watchlist.skeleton.tsx new file mode 100644 index 0000000000..27e8bdb6cd --- /dev/null +++ b/ui/app/(prowler)/_new-overview/watchlist/_components/watchlist.skeleton.tsx @@ -0,0 +1,22 @@ +import { Card, CardContent, CardTitle } from "@/components/shadcn/card/card"; +import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; + +export function WatchlistCardSkeleton() { + return ( + + + + + + + {/* 6 skeleton rows */} + {Array.from({ length: 6 }).map((_, index) => ( +
+ + +
+ ))} +
+
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.ssr.tsx b/ui/app/(prowler)/_new-overview/watchlist/compliance-watchlist.ssr.tsx similarity index 76% rename from ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.ssr.tsx rename to ui/app/(prowler)/_new-overview/watchlist/compliance-watchlist.ssr.tsx index 8d0a832399..fb56458022 100644 --- a/ui/app/(prowler)/_new-overview/components/watchlist/compliance-watchlist.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/watchlist/compliance-watchlist.ssr.tsx @@ -2,16 +2,14 @@ import { adaptComplianceOverviewsResponse, getCompliancesOverview, } from "@/actions/compliances"; -import { SearchParamsProps } from "@/types"; -import { pickFilterParams } from "../../lib/filter-params"; -import { ComplianceWatchlist } from "./compliance-watchlist"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { ComplianceWatchlist } from "./_components/compliance-watchlist"; export const ComplianceWatchlistSSR = async ({ searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +}: SSRComponentProps) => { const filters = pickFilterParams(searchParams); const response = await getCompliancesOverview({ filters }); diff --git a/ui/app/(prowler)/_new-overview/watchlist/index.ts b/ui/app/(prowler)/_new-overview/watchlist/index.ts new file mode 100644 index 0000000000..b2c6f46038 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/watchlist/index.ts @@ -0,0 +1,12 @@ +export type { ComplianceData } from "./_components/compliance-watchlist"; +export { ComplianceWatchlist } from "./_components/compliance-watchlist"; +export { ServiceWatchlist } from "./_components/service-watchlist"; +export { SortToggleButton } from "./_components/sort-toggle-button"; +export { WatchlistCardSkeleton } from "./_components/watchlist.skeleton"; +export { + WatchlistCard, + type WatchlistCardProps, + type WatchlistItem, +} from "./_components/watchlist-card"; +export { ComplianceWatchlistSSR } from "./compliance-watchlist.ssr"; +export { ServiceWatchlistSSR } from "./service-watchlist.ssr"; diff --git a/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.ssr.tsx b/ui/app/(prowler)/_new-overview/watchlist/service-watchlist.ssr.tsx similarity index 60% rename from ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.ssr.tsx rename to ui/app/(prowler)/_new-overview/watchlist/service-watchlist.ssr.tsx index 102a1b0960..9ec08a6cb1 100644 --- a/ui/app/(prowler)/_new-overview/components/watchlist/service-watchlist.ssr.tsx +++ b/ui/app/(prowler)/_new-overview/watchlist/service-watchlist.ssr.tsx @@ -1,14 +1,12 @@ import { getServicesOverview, ServiceOverview } from "@/actions/overview"; -import { SearchParamsProps } from "@/types"; -import { pickFilterParams } from "../../lib/filter-params"; -import { ServiceWatchlist } from "./service-watchlist"; +import { pickFilterParams } from "../_lib/filter-params"; +import { SSRComponentProps } from "../_types"; +import { ServiceWatchlist } from "./_components/service-watchlist"; export const ServiceWatchlistSSR = async ({ searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { +}: SSRComponentProps) => { const filters = pickFilterParams(searchParams); const response = await getServicesOverview({ filters }); diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 13876ee96b..eb653a34ae 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -4,28 +4,32 @@ import { getProviders } from "@/actions/providers"; import { ContentLayout } from "@/components/ui"; import { SearchParamsProps } from "@/types"; -import { AccountsSelector } from "./_new-overview/components/accounts-selector"; +import { AccountsSelector } from "./_new-overview/_components/accounts-selector"; +import { ProviderTypeSelector } from "./_new-overview/_components/provider-type-selector"; import { AttackSurfaceSkeleton, AttackSurfaceSSR, -} from "./_new-overview/components/attack-surface"; -import { CheckFindingsSSR } from "./_new-overview/components/check-findings"; -import { GraphsTabsWrapper } from "./_new-overview/components/graphs-tabs/graphs-tabs-wrapper"; -import { RiskPipelineViewSkeleton } from "./_new-overview/components/graphs-tabs/risk-pipeline-view"; -import { ProviderTypeSelector } from "./_new-overview/components/provider-type-selector"; +} from "./_new-overview/attack-surface"; +import { CheckFindingsSSR } from "./_new-overview/check-findings"; +import { GraphsTabsWrapper } from "./_new-overview/graphs-tabs/graphs-tabs-wrapper"; +import { RiskPipelineViewSkeleton } from "./_new-overview/graphs-tabs/risk-pipeline-view"; import { RiskSeverityChartSkeleton, RiskSeverityChartSSR, -} from "./_new-overview/components/risk-severity-chart"; -import { StatusChartSkeleton } from "./_new-overview/components/status-chart"; +} from "./_new-overview/risk-severity"; +import { + FindingSeverityOverTimeSkeleton, + FindingSeverityOverTimeSSR, +} from "./_new-overview/severity-over-time/finding-severity-over-time.ssr"; +import { StatusChartSkeleton } from "./_new-overview/status-chart"; import { ThreatScoreSkeleton, ThreatScoreSSR, -} from "./_new-overview/components/threat-score"; +} from "./_new-overview/threat-score"; import { ServiceWatchlistSSR, WatchlistCardSkeleton, -} from "./_new-overview/components/watchlist"; +} from "./_new-overview/watchlist"; export default async function Home({ searchParams, @@ -42,7 +46,7 @@ export default async function Home({ -
+
}> @@ -62,10 +66,13 @@ export default async function Home({
-
+
}> + }> + +
diff --git a/ui/components/filters/custom-date-picker.tsx b/ui/components/filters/custom-date-picker.tsx index ff9cb00a5d..bd28bc3615 100644 --- a/ui/components/filters/custom-date-picker.tsx +++ b/ui/components/filters/custom-date-picker.tsx @@ -4,6 +4,7 @@ import { Button, ButtonGroup } from "@heroui/button"; import { DatePicker } from "@heroui/date-picker"; import { getLocalTimeZone, + parseDate, startOfMonth, startOfWeek, today, @@ -20,7 +21,12 @@ export const CustomDatePicker = () => { const [value, setValue] = React.useState(() => { const dateParam = searchParams.get("filter[inserted_at]"); - return dateParam ? today(getLocalTimeZone()) : null; + if (!dateParam) return null; + try { + return parseDate(dateParam); + } catch { + return null; + } }); const { locale } = useLocale(); diff --git a/ui/components/graphs/line-chart.tsx b/ui/components/graphs/line-chart.tsx index 79f6c3713c..d24890b686 100644 --- a/ui/components/graphs/line-chart.tsx +++ b/ui/components/graphs/line-chart.tsx @@ -19,16 +19,20 @@ import { import { AlertPill } from "./shared/alert-pill"; import { ChartLegend } from "./shared/chart-legend"; +import { CustomActiveDot, PointClickData } from "./shared/custom-active-dot"; import { AXIS_FONT_SIZE, CustomXAxisTickWithToday, } from "./shared/custom-axis-tick"; +import { CustomDot } from "./shared/custom-dot"; import { LineConfig, LineDataPoint } from "./types"; interface LineChartProps { data: LineDataPoint[]; lines: LineConfig[]; height?: number; + xAxisInterval?: number | "preserveStart" | "preserveEnd" | "preserveStartEnd"; + onPointClick?: (data: PointClickData) => void; } interface TooltipPayloadItem { @@ -39,28 +43,54 @@ interface TooltipPayloadItem { payload: LineDataPoint; } +const formatTooltipDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); +}; + +interface CustomLineTooltipProps extends TooltipProps { + filterLine?: string | null; +} + const CustomLineTooltip = ({ active, payload, label, -}: TooltipProps) => { + filterLine, +}: CustomLineTooltipProps) => { if (!active || !payload || payload.length === 0) { return null; } const typedPayload = payload as unknown as TooltipPayloadItem[]; - const totalValue = typedPayload.reduce((sum, item) => sum + item.value, 0); + + // Filter payload if a line is selected or hovered + const displayPayload = filterLine + ? typedPayload.filter((item) => item.dataKey === filterLine) + : typedPayload; + + if (displayPayload.length === 0) { + return null; + } + + const totalValue = displayPayload.reduce((sum, item) => sum + item.value, 0); + const formattedDate = formatTooltipDate(String(label)); return (
-

{label}

+

+ {formattedDate} +

- {typedPayload.map((item) => { + {displayPayload.map((item) => { const newFindings = item.payload[`${item.dataKey}_newFindings`]; const change = item.payload[`${item.dataKey}_change`]; @@ -106,14 +136,30 @@ const chartConfig = { }, } satisfies ChartConfig; -export function LineChart({ data, lines, height = 400 }: LineChartProps) { +export function LineChart({ + data, + lines, + height = 400, + xAxisInterval = "preserveStartEnd", + onPointClick, +}: LineChartProps) { const [hoveredLine, setHoveredLine] = useState(null); + const [selectedLine, setSelectedLine] = useState(null); + + // Active line is either selected (persistent) or hovered (temporary) + const activeLine = selectedLine ?? hoveredLine; const legendItems = lines.map((line) => ({ label: line.label, color: line.color, + dataKey: line.dataKey, })); + const handleLegendClick = (dataKey: string) => { + // Toggle selection: if already selected, deselect; otherwise select + setSelectedLine((current) => (current === dataKey ? null : dataKey)); + }; + return (
( + + )} /> - } /> + } + /> {lines.map((line) => { - const isHovered = hoveredLine === line.dataKey; - const isFaded = hoveredLine !== null && !isHovered; + const isActive = activeLine === line.dataKey; + const isFaded = activeLine !== null && !isActive; return ( setHoveredLine(line.dataKey)} - onMouseLeave={() => setHoveredLine(null)} + dot={({ + key, + ...props + }: { + key?: string; + cx?: number; + cy?: number; + }) => ( + + )} + activeDot={(props: { + cx?: number; + cy?: number; + payload?: LineDataPoint; + }) => ( + setHoveredLine(line.dataKey)} + onMouseLeave={() => setHoveredLine(null)} + /> + )} style={{ transition: "stroke-opacity 0.2s" }} /> ); @@ -175,8 +258,15 @@ export function LineChart({ data, lines, height = 400 }: LineChartProps) { -
- +
+

+ Click to filter by severity. +

+
); diff --git a/ui/components/graphs/shared/chart-legend.tsx b/ui/components/graphs/shared/chart-legend.tsx index 759b8db09c..f05bfbad57 100644 --- a/ui/components/graphs/shared/chart-legend.tsx +++ b/ui/components/graphs/shared/chart-legend.tsx @@ -1,29 +1,55 @@ export interface ChartLegendItem { label: string; color: string; + dataKey?: string; } interface ChartLegendProps { items: ChartLegendItem[]; + selectedItem?: string | null; + onItemClick?: (dataKey: string) => void; } -export function ChartLegend({ items }: ChartLegendProps) { +export function ChartLegend({ + items, + selectedItem, + onItemClick, +}: ChartLegendProps) { + const isInteractive = !!onItemClick; + return (
- {items.map((item, index) => ( -
-
- - {item.label} - -
- ))} + {items.map((item, index) => { + const dataKey = item.dataKey ?? item.label.toLowerCase(); + const isSelected = selectedItem === dataKey; + const isFaded = selectedItem !== null && !isSelected; + + return ( + + ); + })}
); } diff --git a/ui/components/graphs/shared/custom-active-dot.tsx b/ui/components/graphs/shared/custom-active-dot.tsx new file mode 100644 index 0000000000..fb12b4d02b --- /dev/null +++ b/ui/components/graphs/shared/custom-active-dot.tsx @@ -0,0 +1,54 @@ +import { Dot } from "recharts"; + +import { LineDataPoint } from "../types"; + +export interface PointClickData { + point: LineDataPoint; + dataKey?: string; +} + +interface CustomActiveDotProps { + cx?: number; + cy?: number; + payload?: LineDataPoint; + dataKey: string; + color: string; + isFaded: boolean; + onPointClick?: (data: PointClickData) => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +export const CustomActiveDot = ({ + cx, + cy, + payload, + dataKey, + color, + isFaded, + onPointClick, + onMouseEnter, + onMouseLeave, +}: CustomActiveDotProps) => { + if (cx === undefined || cy === undefined) return null; + + // Don't render active dot for faded lines + if (isFaded) return null; + + return ( + { + if (onPointClick && payload) { + onPointClick({ point: payload, dataKey }); + } + }} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + /> + ); +}; diff --git a/ui/components/graphs/shared/custom-axis-tick.tsx b/ui/components/graphs/shared/custom-axis-tick.tsx index 8c05529356..858bd8cee2 100644 --- a/ui/components/graphs/shared/custom-axis-tick.tsx +++ b/ui/components/graphs/shared/custom-axis-tick.tsx @@ -1,27 +1,55 @@ export const AXIS_FONT_SIZE = 14; const TODAY_FONT_SIZE = 12; +const MONTH_FONT_SIZE = 11; interface CustomXAxisTickProps { x: number; y: number; + index?: number; payload: { value: string | number; }; + visibleTicksCount?: number; } -const getTodayFormatted = () => { +const getTodayISO = () => { const today = new Date(); - return today.toLocaleDateString("en-US", { - month: "2-digit", - day: "2-digit", - }); + return today.toISOString().split("T")[0]; +}; + +const getMonthName = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { month: "short" }); +}; + +const getDayNumber = (dateStr: string) => { + const date = new Date(dateStr); + return date.getDate(); +}; + +const getMonthFromDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.getMonth(); }; export const CustomXAxisTickWithToday = Object.assign( - function CustomXAxisTickWithToday(props: CustomXAxisTickProps) { - const { x, y, payload } = props; - const todayFormatted = getTodayFormatted(); - const isToday = String(payload.value) === todayFormatted; + function CustomXAxisTickWithToday( + props: CustomXAxisTickProps & { data?: Array<{ date: string }> }, + ) { + const { x, y, payload, index = 0, data = [] } = props; + const dateStr = String(payload.value); + const todayISO = getTodayISO(); + const isToday = dateStr === todayISO; + + const dayNumber = getDayNumber(dateStr); + const currentMonth = getMonthFromDate(dateStr); + + // Show month name if it's the first tick or if the month changed from previous tick + const isFirstTick = index === 0; + const previousDate = index > 0 && data[index - 1]?.date; + const previousMonth = previousDate ? getMonthFromDate(previousDate) : -1; + const monthChanged = currentMonth !== previousMonth; + const showMonth = isFirstTick || monthChanged; return ( @@ -33,12 +61,23 @@ export const CustomXAxisTickWithToday = Object.assign( fill="var(--color-text-neutral-secondary)" fontSize={AXIS_FONT_SIZE} > - {payload.value} + {dayNumber} + {showMonth && ( + + {getMonthName(dateStr)} + + )} {isToday && ( { + if (cx === undefined || cy === undefined) return null; + + return ; +}; diff --git a/ui/components/graphs/types.ts b/ui/components/graphs/types.ts index 46fabbdaf1..9ec1c1d4a4 100644 --- a/ui/components/graphs/types.ts +++ b/ui/components/graphs/types.ts @@ -27,7 +27,7 @@ export interface DonutDataPoint { export interface LineDataPoint { date: string; - [key: string]: string | number; + [key: string]: string | number | string[]; } export interface RadarDataPoint { @@ -60,5 +60,5 @@ export interface TooltipData { new?: number; muted?: number; change?: number; - [key: string]: any; + [key: string]: string | number | boolean | string[] | undefined; } diff --git a/ui/lib/lighthouse/tools/overview.ts b/ui/lib/lighthouse/tools/overview.ts index 93940f2ef0..cf5f096034 100644 --- a/ui/lib/lighthouse/tools/overview.ts +++ b/ui/lib/lighthouse/tools/overview.ts @@ -5,7 +5,7 @@ import { getFindingsBySeverity, getFindingsByStatus, getProvidersOverview, -} from "@/actions/overview/overview"; +} from "@/actions/overview"; import { getFindingsBySeveritySchema, getFindingsByStatusSchema, diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index f3a322602a..e334d70d21 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -21,6 +21,30 @@ export const mapProviderFiltersForFindings = ( } }; +/** + * Maps overview provider filters to findings page provider filters (object version). + * Converts provider_id__in to provider__in and removes provider_type__in + * since provider__in is more specific. + */ +export const mapProviderFiltersForFindingsObject = < + T extends Record, +>( + filters: T, +): T => { + const result = { ...filters }; + const providerIdKey = "filter[provider_id__in]"; + const providerTypeKey = "filter[provider_type__in]"; + const providerKey = "filter[provider__in]"; + + if (providerIdKey in result) { + result[providerKey as keyof T] = result[providerIdKey as keyof T]; + delete result[providerIdKey as keyof T]; + delete result[providerTypeKey as keyof T]; + } + + return result; +}; + export const extractProviderUIDs = ( providersData: ProvidersApiResponse, ): string[] => { diff --git a/ui/types/severities.ts b/ui/types/severities.ts index 1f37688502..7808ea1909 100644 --- a/ui/types/severities.ts +++ b/ui/types/severities.ts @@ -25,6 +25,9 @@ export const SEVERITY_COLORS: Record = { informational: "var(--color-bg-data-info)", }; +// Muted color for charts - uses CSS var() for Recharts inline style compatibility (same pattern as SEVERITY_COLORS) +export const MUTED_COLOR = "var(--color-bg-data-muted)"; + export const SEVERITY_FILTER_MAP: Record = { Critical: "critical", High: "high",