mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-26 21:34:19 +00:00
feat(attack-paths): merge master for updating boto3 and botocore
This commit is contained in:
164 files changed
+5665
-1344
No files matched your search
+14
-1
@@ -10,7 +10,8 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- New findings table UI with new design system components, improved filtering UX, and enhanced table interactions [(#9699)](https://github.com/prowler-cloud/prowler/pull/9699)
|
||||
- Add gradient background to Risk Plot for visual risk context [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
|
||||
- Add ThreatScore pillar breakdown to Compliance Summary page and detail view [(#9773)](https://github.com/prowler-cloud/prowler/pull/9773)
|
||||
- Add a new main section for list Attack Paths scans, execute queries on them and view their result as a graph [(#)](https://github.com/prowler-cloud/prowler/pull/)
|
||||
- Compliance Watchlist component in Overview page [(#9786)](https://github.com/prowler-cloud/prowler/pull/9786)
|
||||
- - Add a new main section for list Attack Paths scans, execute queries on them and view their result as a graph [(#)](https://github.com/prowler-cloud/prowler/pull/)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -20,6 +21,18 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Remove unused hasDots prop from RadialChart component [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
|
||||
- Add showCenterLabel prop to RadialChart for optional center text display [(#9773)](https://github.com/prowler-cloud/prowler/pull/9773)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- Upgrade Node.js from 20.x to 24.13.0 LTS, patching 8 CVEs from January 2026 security advisory [(#9797)](https://github.com/prowler-cloud/prowler/pull/9797)
|
||||
|
||||
---
|
||||
|
||||
## [1.16.2] (Prowler v5.16.2) (UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- OCI update credentials form failing silently due to missing provider UID [(#9746)](https://github.com/prowler-cloud/prowler/pull/9746)
|
||||
|
||||
---
|
||||
|
||||
## [1.16.1] (Prowler v5.16.1)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS base
|
||||
FROM node:24.13.0-alpine AS base
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud"
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { formatLabel } from "@/lib/categories";
|
||||
|
||||
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
|
||||
|
||||
export interface EnrichedComplianceWatchlistItem {
|
||||
id: string;
|
||||
complianceId: string;
|
||||
label: string;
|
||||
icon: ReturnType<typeof getComplianceIcon>;
|
||||
score: number;
|
||||
requirementsPassed: number;
|
||||
requirementsFailed: number;
|
||||
requirementsManual: number;
|
||||
totalRequirements: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats compliance_id into a human-readable label
|
||||
* e.g., "aws_account_security_onboarding_aws" → "AWS Account Security Onboarding"
|
||||
*
|
||||
* Uses the shared formatLabel utility from lib/categories.ts which handles:
|
||||
* - Acronyms (≤3 chars like AWS, CIS, ISO, PCI, SOC, etc.)
|
||||
* - Special cases (4+ char acronyms like GDPR, HIPAA, NIST, etc.)
|
||||
* - Version patterns (e.g., "v1", "v2")
|
||||
*/
|
||||
function formatComplianceLabel(complianceId: string): string {
|
||||
// Remove trailing provider suffix (e.g., "_aws", "_gcp", "_azure")
|
||||
const withoutProvider = complianceId
|
||||
.replace(/_aws$/i, "")
|
||||
.replace(/_gcp$/i, "")
|
||||
.replace(/_azure$/i, "")
|
||||
.replace(/_kubernetes$/i, "");
|
||||
|
||||
return formatLabel(withoutProvider, "_");
|
||||
}
|
||||
|
||||
export function adaptComplianceWatchlistResponse(
|
||||
response: ComplianceWatchlistResponse | undefined,
|
||||
): EnrichedComplianceWatchlistItem[] {
|
||||
if (!response?.data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.data.map((item) => {
|
||||
const {
|
||||
compliance_id,
|
||||
requirements_passed,
|
||||
requirements_failed,
|
||||
requirements_manual,
|
||||
total_requirements,
|
||||
} = item.attributes;
|
||||
|
||||
// Defensive conversion: API types are number but JSON parsing edge cases may return strings
|
||||
const totalReqs = Number(total_requirements) || 0;
|
||||
const passedReqs = Number(requirements_passed) || 0;
|
||||
const score =
|
||||
totalReqs > 0 ? Math.round((passedReqs / totalReqs) * 100) : 0;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
complianceId: compliance_id,
|
||||
label: formatComplianceLabel(compliance_id),
|
||||
icon: getComplianceIcon(compliance_id),
|
||||
score,
|
||||
requirementsPassed: requirements_passed,
|
||||
requirementsFailed: requirements_failed,
|
||||
requirementsManual: requirements_manual,
|
||||
totalRequirements: total_requirements,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
|
||||
|
||||
export const getComplianceWatchlist = async ({
|
||||
filters = {},
|
||||
}: {
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}): Promise<ComplianceWatchlistResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
const url = new URL(`${apiBaseUrl}/overviews/compliance-watchlist`);
|
||||
|
||||
// Append filter parameters (provider_id, provider_type, etc.)
|
||||
// Exclude filter[search] as this endpoint doesn't support text search
|
||||
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 compliance watchlist:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const COMPLIANCE_WATCHLIST_OVERVIEW_TYPE = {
|
||||
WATCHLIST_OVERVIEW: "compliance-watchlist-overviews",
|
||||
} as const;
|
||||
|
||||
type ComplianceWatchlistOverviewType =
|
||||
(typeof COMPLIANCE_WATCHLIST_OVERVIEW_TYPE)[keyof typeof COMPLIANCE_WATCHLIST_OVERVIEW_TYPE];
|
||||
|
||||
export interface ComplianceWatchlistOverviewAttributes {
|
||||
compliance_id: string;
|
||||
requirements_passed: number;
|
||||
requirements_failed: number;
|
||||
requirements_manual: number;
|
||||
total_requirements: number;
|
||||
}
|
||||
|
||||
export interface ComplianceWatchlistOverview {
|
||||
type: ComplianceWatchlistOverviewType;
|
||||
id: string;
|
||||
attributes: ComplianceWatchlistOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ComplianceWatchlistResponse {
|
||||
data: ComplianceWatchlistOverview[];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { getComplianceWatchlist } from "./compliance-watchlist";
|
||||
export {
|
||||
adaptComplianceWatchlistResponse,
|
||||
type EnrichedComplianceWatchlistItem,
|
||||
} from "./compliance-watchlist.adapter";
|
||||
export type {
|
||||
ComplianceWatchlistOverview,
|
||||
ComplianceWatchlistResponse,
|
||||
} from "./compliance-watchlist.types";
|
||||
@@ -14,11 +14,16 @@ export interface ComplianceData {
|
||||
score: number;
|
||||
}
|
||||
|
||||
// Display 7 items to match the card's min-height (405px) without scrolling
|
||||
const ITEMS_TO_DISPLAY = 7;
|
||||
|
||||
export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
const [isAsc, setIsAsc] = useState(true);
|
||||
|
||||
// Sort all items and take top 7 based on current sort order
|
||||
const sortedItems = [...items]
|
||||
.sort((a, b) => (isAsc ? a.score - b.score : b.score - a.score))
|
||||
.slice(0, ITEMS_TO_DISPLAY)
|
||||
.map((item) => ({
|
||||
key: item.id,
|
||||
icon: item.icon ? (
|
||||
@@ -41,7 +46,7 @@ export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
<WatchlistCard
|
||||
title="Compliance Watchlist"
|
||||
items={sortedItems}
|
||||
ctaLabel="Compliance Dashboard"
|
||||
ctaLabel="Explore Compliance for Each Scan"
|
||||
ctaHref="/compliance"
|
||||
headerAction={
|
||||
<SortToggleButton
|
||||
@@ -51,10 +56,10 @@ export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
descendingLabel="Sort by lowest score"
|
||||
/>
|
||||
}
|
||||
// TODO: Enable full emptyState with description once API endpoint is implemented
|
||||
// Full emptyState: { message: "...", description: "to add compliance frameworks to your watchlist.", linkText: "Compliance Dashboard" }
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
description: "to add compliance frameworks to your watchlist.",
|
||||
linkText: "Compliance Dashboard",
|
||||
message: "No compliance data available.",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -85,12 +85,15 @@ export const WatchlistCard = ({
|
||||
const isEmpty = items.length === 0;
|
||||
|
||||
return (
|
||||
<Card variant="base" className="flex min-h-[405px] min-w-[312px] flex-col">
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[405px] w-full flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{headerAction}
|
||||
</div>
|
||||
<CardContent className="flex flex-col">
|
||||
<CardContent className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{isEmpty ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-12 py-6">
|
||||
{/* Icon and message */}
|
||||
@@ -102,19 +105,15 @@ export const WatchlistCard = ({
|
||||
</div>
|
||||
|
||||
{/* Description with link */}
|
||||
<p className="text-text-neutral-tertiary w-full text-sm leading-6">
|
||||
{emptyState?.description && ctaHref && (
|
||||
<>
|
||||
Visit the{" "}
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<Link href={ctaHref}>
|
||||
{emptyState.linkText || ctaLabel}
|
||||
</Link>
|
||||
</Button>{" "}
|
||||
{emptyState.description}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{emptyState?.description && ctaHref && (
|
||||
<p className="text-text-neutral-tertiary w-full text-sm leading-6">
|
||||
Visit the{" "}
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<Link href={ctaHref}>{emptyState.linkText || ctaLabel}</Link>
|
||||
</Button>{" "}
|
||||
{emptyState.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -149,7 +148,7 @@ export const WatchlistCard = ({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-[54px] items-center justify-between gap-2 px-3 py-[11px]",
|
||||
"flex h-[54px] min-w-0 items-center justify-between gap-2 px-3 py-[11px]",
|
||||
!isLast && "border-border-neutral-tertiary border-b",
|
||||
isClickable &&
|
||||
"hover:bg-bg-neutral-tertiary cursor-pointer",
|
||||
@@ -161,10 +160,10 @@ export const WatchlistCard = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-text-neutral-secondary flex-1 truncate text-sm leading-6">
|
||||
<p className="text-text-neutral-secondary w-0 flex-1 truncate text-sm leading-6">
|
||||
{item.label}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm leading-6 font-bold",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
adaptComplianceOverviewsResponse,
|
||||
getCompliancesOverview,
|
||||
} from "@/actions/compliances";
|
||||
adaptComplianceWatchlistResponse,
|
||||
getComplianceWatchlist,
|
||||
} from "@/actions/overview/compliance-watchlist";
|
||||
|
||||
import { pickFilterParams } from "../_lib/filter-params";
|
||||
import { SSRComponentProps } from "../_types";
|
||||
@@ -11,20 +11,19 @@ export const ComplianceWatchlistSSR = async ({
|
||||
searchParams,
|
||||
}: SSRComponentProps) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
const response = await getComplianceWatchlist({ filters });
|
||||
const enrichedData = adaptComplianceWatchlistResponse(response);
|
||||
|
||||
const response = await getCompliancesOverview({ filters });
|
||||
const { data } = adaptComplianceOverviewsResponse(response);
|
||||
|
||||
// Filter out ProwlerThreatScore and limit to 5 items
|
||||
const items = data
|
||||
.filter((item) => item.framework !== "ProwlerThreatScore")
|
||||
.slice(0, 5)
|
||||
.map((compliance) => ({
|
||||
id: compliance.id,
|
||||
framework: compliance.framework,
|
||||
label: compliance.label,
|
||||
icon: compliance.icon,
|
||||
score: compliance.score,
|
||||
// Filter out ProwlerThreatScore and pass all items to client
|
||||
// Client handles sorting and limiting to display count
|
||||
const items = enrichedData
|
||||
.filter((item) => !item.complianceId.toLowerCase().includes("threatscore"))
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
framework: item.complianceId,
|
||||
label: item.label,
|
||||
icon: item.icon,
|
||||
score: item.score,
|
||||
}));
|
||||
|
||||
return <ComplianceWatchlist items={items} />;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import {
|
||||
@@ -150,19 +149,20 @@ export default async function Findings({
|
||||
|
||||
return (
|
||||
<ContentLayout title="Findings" icon="lucide:tag">
|
||||
<FindingsFilters
|
||||
providers={providersData?.data || []}
|
||||
providerIds={providerIds}
|
||||
providerDetails={providerDetails}
|
||||
completedScans={completedScans || []}
|
||||
completedScanIds={completedScanIds}
|
||||
scanDetails={scanDetails}
|
||||
uniqueRegions={uniqueRegions}
|
||||
uniqueServices={uniqueServices}
|
||||
uniqueResourceTypes={uniqueResourceTypes}
|
||||
uniqueCategories={uniqueCategories}
|
||||
/>
|
||||
<Spacer y={8} />
|
||||
<div className="mb-6">
|
||||
<FindingsFilters
|
||||
providers={providersData?.data || []}
|
||||
providerIds={providerIds}
|
||||
providerDetails={providerDetails}
|
||||
completedScans={completedScans || []}
|
||||
completedScanIds={completedScanIds}
|
||||
scanDetails={scanDetails}
|
||||
uniqueRegions={uniqueRegions}
|
||||
uniqueServices={uniqueServices}
|
||||
uniqueResourceTypes={uniqueResourceTypes}
|
||||
uniqueCategories={uniqueCategories}
|
||||
/>
|
||||
</div>
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
|
||||
<SSRDataTable searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
@@ -91,11 +91,7 @@ export const createMuteRulesColumns = (
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (
|
||||
<div className="flex items-center justify-center px-2">
|
||||
<span className="text-sm font-semibold">Actions</span>
|
||||
</div>
|
||||
),
|
||||
header: () => null,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<MuteRuleRowActions
|
||||
|
||||
+24
-12
@@ -24,6 +24,7 @@ import {
|
||||
import { StatusChartSkeleton } from "./_overview/status-chart";
|
||||
import { ThreatScoreSkeleton, ThreatScoreSSR } from "./_overview/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
ServiceWatchlistSSR,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./_overview/watchlist";
|
||||
@@ -57,19 +58,30 @@ export default async function Home({
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Suspense fallback={<AttackSurfaceSkeleton />}>
|
||||
<AttackSurfaceSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-6 xl:flex-row">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FindingSeverityOverTimeSkeleton />}>
|
||||
<FindingSeverityOverTimeSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
{/* Watchlists: stacked on mobile, row on tablet, stacked on desktop */}
|
||||
<div className="flex min-w-0 flex-col gap-6 overflow-hidden sm:flex-row sm:flex-wrap sm:items-stretch xl:w-[312px] xl:shrink-0 xl:flex-col">
|
||||
<div className="min-w-0 sm:flex-1 xl:flex-auto [&>*]:h-full">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="min-w-0 sm:flex-1 xl:flex-auto [&>*]:h-full">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts column: Attack Surface on top, Findings Over Time below */}
|
||||
<div className="flex flex-1 flex-col gap-6">
|
||||
<Suspense fallback={<AttackSurfaceSkeleton />}>
|
||||
<AttackSurfaceSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FindingSeverityOverTimeSkeleton />}>
|
||||
<FindingSeverityOverTimeSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { getProvider } from "@/actions/providers/providers";
|
||||
import { CredentialsUpdateInfo } from "@/components/providers";
|
||||
import {
|
||||
UpdateViaCredentialsForm,
|
||||
@@ -20,9 +22,24 @@ interface Props {
|
||||
|
||||
export default async function UpdateCredentialsPage({ searchParams }: Props) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const { type: providerType, via } = resolvedSearchParams;
|
||||
const { type: providerType, via, id: providerId } = resolvedSearchParams;
|
||||
|
||||
if (!providerId) {
|
||||
redirect("/providers");
|
||||
}
|
||||
|
||||
const formType = getProviderFormType(providerType, via);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
const providerResponse = await getProvider(formData);
|
||||
|
||||
if (providerResponse?.errors) {
|
||||
redirect("/providers");
|
||||
}
|
||||
|
||||
const providerUid = providerResponse?.data?.attributes?.uid;
|
||||
|
||||
switch (formType) {
|
||||
case "selector":
|
||||
return (
|
||||
@@ -30,14 +47,27 @@ export default async function UpdateCredentialsPage({ searchParams }: Props) {
|
||||
);
|
||||
|
||||
case "credentials":
|
||||
return <UpdateViaCredentialsForm searchParams={resolvedSearchParams} />;
|
||||
return (
|
||||
<UpdateViaCredentialsForm
|
||||
searchParams={resolvedSearchParams}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
);
|
||||
|
||||
case "role":
|
||||
return <UpdateViaRoleForm searchParams={resolvedSearchParams} />;
|
||||
return (
|
||||
<UpdateViaRoleForm
|
||||
searchParams={resolvedSearchParams}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
);
|
||||
|
||||
case "service-account":
|
||||
return (
|
||||
<UpdateViaServiceAccountForm searchParams={resolvedSearchParams} />
|
||||
<UpdateViaServiceAccountForm
|
||||
searchParams={resolvedSearchParams}
|
||||
providerUid={providerUid}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { XCircle } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { useUrlFilters } from "@/hooks/use-url-filters";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { Button } from "../shadcn";
|
||||
|
||||
// Filters that should be excluded from count and visibility check
|
||||
const EXCLUDED_FILTERS = ["filter[search]", "filter[muted]"];
|
||||
|
||||
export interface ClearFiltersButtonProps {
|
||||
className?: string;
|
||||
text?: string;
|
||||
@@ -23,22 +25,45 @@ export const ClearFiltersButton = ({
|
||||
showCount = false,
|
||||
variant = "link",
|
||||
}: ClearFiltersButtonProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { clearAllFilters, hasFilters } = useUrlFilters();
|
||||
|
||||
// Count active filters (excluding search)
|
||||
const filterCount = Array.from(searchParams.keys()).filter(
|
||||
(key) => key.startsWith("filter[") && key !== "filter[search]",
|
||||
).length;
|
||||
// Get active filters (excluding search and muted)
|
||||
const activeFilters = Array.from(searchParams.keys()).filter(
|
||||
(key) => key.startsWith("filter[") && !EXCLUDED_FILTERS.includes(key),
|
||||
);
|
||||
|
||||
if (!hasFilters()) {
|
||||
const filterCount = activeFilters.length;
|
||||
|
||||
// Clear all filters except excluded ones (muted, search)
|
||||
const clearFiltersPreservingExcluded = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
Array.from(params.keys()).forEach((key) => {
|
||||
if (
|
||||
(key.startsWith("filter[") && !EXCLUDED_FILTERS.includes(key)) ||
|
||||
key === "sort"
|
||||
) {
|
||||
params.delete(key);
|
||||
}
|
||||
});
|
||||
params.delete("page");
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
}, [router, searchParams, pathname]);
|
||||
|
||||
// Only show button if there are filters other than the excluded ones
|
||||
if (filterCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayText = showCount ? `Clear Filters (${filterCount})` : text;
|
||||
|
||||
return (
|
||||
<Button aria-label={ariaLabel} onClick={clearAllFilters} variant={variant}>
|
||||
<Button
|
||||
aria-label={ariaLabel}
|
||||
onClick={clearFiltersPreservingExcluded}
|
||||
variant={variant}
|
||||
>
|
||||
<XCircle className="mr-0.5 size-4" />
|
||||
{displayText}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
|
||||
import { FilterOption } from "@/types";
|
||||
|
||||
import { DataTableFilterCustom } from "../ui/table";
|
||||
@@ -32,7 +30,7 @@ export const FilterControls = ({
|
||||
customFilters,
|
||||
}: FilterControlsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<div className="flex flex-col items-start gap-4 md:flex-row md:items-center">
|
||||
<div className="grid w-full flex-1 grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{search && <CustomSearchInput />}
|
||||
@@ -45,7 +43,6 @@ export const FilterControls = ({
|
||||
</div>
|
||||
{customFilters && customFilters.length > 0 && (
|
||||
<>
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom filters={customFilters} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -287,6 +287,23 @@ export function getColumnFindings(
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
// Region column
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Region" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
const regionText = typeof region === "string" ? region : "-";
|
||||
return (
|
||||
<p className="text-text-neutral-primary max-w-[120px] truncate text-sm">
|
||||
{regionText}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
// TODO: PROWLER-379 - Enable Impacted Resources column when backend supports grouped findings
|
||||
// {
|
||||
// accessorKey: "impactedResources",
|
||||
|
||||
@@ -182,7 +182,8 @@ export const FindingDetail = ({
|
||||
|
||||
{/* Row 3: First Seen */}
|
||||
<div className="text-text-neutral-tertiary text-sm">
|
||||
<DateWithTime inline dateTime={attributes.first_seen_at || "-"} />
|
||||
<span className="text-text-neutral-secondary mr-1">Time:</span>
|
||||
<DateWithTime inline dateTime={attributes.updated_at || "-"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,6 +224,9 @@ export const FindingDetail = ({
|
||||
<InfoField label="Finding UID" variant="simple">
|
||||
<CodeSnippet value={attributes.uid} />
|
||||
</InfoField>
|
||||
<InfoField label="First seen" variant="simple">
|
||||
<DateWithTime inline dateTime={attributes.first_seen_at || "-"} />
|
||||
</InfoField>
|
||||
</div>
|
||||
|
||||
{attributes.status === "FAIL" && (
|
||||
|
||||
@@ -34,12 +34,14 @@ const InfoField = ({
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="text-text-neutral-secondary text-xs font-bold">
|
||||
{label}
|
||||
</span>
|
||||
<div className="border-border-input-primary bg-bg-input-primary flex items-center rounded-lg border p-3">
|
||||
<span className="text-small text-text-neutral-primary">{children}</span>
|
||||
<div className="border-border-input-primary bg-bg-input-primary flex min-w-0 items-center overflow-hidden rounded-lg border p-3">
|
||||
<span className="text-small text-text-neutral-primary min-w-0 truncate">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -87,18 +89,18 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
|
||||
Share this link with the user:
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col items-start justify-between">
|
||||
<div className="flex w-full flex-col items-start justify-between overflow-hidden">
|
||||
<Snippet
|
||||
classNames={{
|
||||
base: "mx-auto",
|
||||
base: "w-full max-w-full",
|
||||
content: "min-w-0 overflow-hidden",
|
||||
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
|
||||
}}
|
||||
hideSymbol
|
||||
variant="bordered"
|
||||
className="bg-bg-neutral-secondary overflow-hidden py-1 text-ellipsis whitespace-nowrap"
|
||||
className="bg-bg-neutral-secondary max-w-full overflow-hidden py-1"
|
||||
>
|
||||
<p className="no-scrollbar w-fit overflow-hidden overflow-x-scroll text-sm text-ellipsis whitespace-nowrap">
|
||||
{invitationLink}
|
||||
</p>
|
||||
<p className="min-w-0 truncate text-sm">{invitationLink}</p>
|
||||
</Snippet>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -77,9 +77,7 @@ export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
header: () => null,
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const roles = row.original.roles;
|
||||
|
||||
@@ -1,227 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Database } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ColumnDef, RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { Muted } from "@/components/findings/muted";
|
||||
import { DataTableRowDetails } from "@/components/findings/table";
|
||||
import { DeltaIndicator } from "@/components/findings/table/delta-indicator";
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import {
|
||||
DateWithTime,
|
||||
EntityInfo,
|
||||
SnippetChip,
|
||||
} from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import {
|
||||
DataTableColumnHeader,
|
||||
SeverityBadge,
|
||||
StatusFindingBadge,
|
||||
} from "@/components/ui/table";
|
||||
import { FindingProps, ProviderType } from "@/types";
|
||||
import { getColumnFindings } from "@/components/findings/table/column-findings";
|
||||
import { FindingProps } from "@/types";
|
||||
|
||||
const getFindingsData = (row: { original: FindingProps }) => {
|
||||
return row.original;
|
||||
};
|
||||
const baseColumns: ColumnDef<FindingProps>[] = getColumnFindings(
|
||||
{} as RowSelectionState,
|
||||
0,
|
||||
).filter((column) => column.id !== "select" && column.id !== "actions");
|
||||
|
||||
const getFindingsMetadata = (row: { original: FindingProps }) => {
|
||||
return row.original.attributes.check_metadata;
|
||||
};
|
||||
|
||||
const getResourceData = (
|
||||
row: { original: FindingProps },
|
||||
field: keyof FindingProps["relationships"]["resource"]["attributes"],
|
||||
) => {
|
||||
return (
|
||||
row.original.relationships?.resource?.attributes?.[field] ||
|
||||
`No ${field} found in resource`
|
||||
);
|
||||
};
|
||||
|
||||
const getProviderData = (
|
||||
row: { original: FindingProps },
|
||||
field: keyof FindingProps["relationships"]["provider"]["attributes"],
|
||||
) => {
|
||||
return (
|
||||
row.original.relationships?.provider?.attributes?.[field] ||
|
||||
`No ${field} found in provider`
|
||||
);
|
||||
};
|
||||
|
||||
const FindingDetailsCell = ({ row }: { row: any }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const findingId = searchParams.get("id");
|
||||
const isOpen = findingId === row.original.id;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<TriggerSheet
|
||||
triggerComponent={
|
||||
<InfoIcon className="text-button-primary" size={16} />
|
||||
}
|
||||
title="Finding Details"
|
||||
description="View the finding details"
|
||||
defaultOpen={isOpen}
|
||||
>
|
||||
<DataTableRowDetails
|
||||
entityId={row.original.id}
|
||||
findingDetails={row.original}
|
||||
/>
|
||||
</TriggerSheet>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Details" />
|
||||
),
|
||||
cell: ({ row }) => <FindingDetailsCell row={row} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "check",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Finding" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
const {
|
||||
attributes: { muted, muted_reason },
|
||||
} = getFindingsData(row);
|
||||
const { delta } = row.original.attributes;
|
||||
return (
|
||||
<div className="3xl:max-w-[660px] relative flex max-w-[410px] flex-row items-center gap-2">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{delta === "new" || delta === "changed" ? (
|
||||
<DeltaIndicator delta={delta} />
|
||||
) : (
|
||||
<div className="w-2" />
|
||||
)}
|
||||
<p className="mr-7 text-sm break-words whitespace-normal">
|
||||
{checktitle}
|
||||
</p>
|
||||
</div>
|
||||
<span className="absolute top-1/2 -right-2 -translate-y-1/2">
|
||||
<Muted isMuted={muted} mutedReason={muted_reason || ""} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Resource name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceName = getResourceData(row, "name");
|
||||
|
||||
return (
|
||||
<SnippetChip
|
||||
value={resourceName as string}
|
||||
formatter={(value) => `...${value.slice(-10)}`}
|
||||
icon={<Database size={16} />}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "severity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Severity" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { severity },
|
||||
} = getFindingsData(row);
|
||||
return <SeverityBadge severity={severity} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { status },
|
||||
} = getFindingsData(row);
|
||||
|
||||
return <StatusFindingBadge size="sm" status={status} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "updated_at",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Last seen" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { updated_at },
|
||||
} = getFindingsData(row);
|
||||
return (
|
||||
<div className="w-[100px]">
|
||||
<DateWithTime dateTime={updated_at} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Region" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
return (
|
||||
<div className="w-[80px]">
|
||||
{typeof region === "string" ? region : "Invalid region"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "service",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { servicename } = getFindingsMetadata(row);
|
||||
return <p className="text-small max-w-96 truncate">{servicename}</p>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "cloudProvider",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cloud Provider" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const provider = getProviderData(row, "provider");
|
||||
const alias = getProviderData(row, "alias");
|
||||
const uid = getProviderData(row, "uid");
|
||||
|
||||
return (
|
||||
<>
|
||||
<EntityInfo
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = baseColumns;
|
||||
@@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";
|
||||
|
||||
export const UpdateViaCredentialsForm = ({
|
||||
searchParams,
|
||||
providerUid,
|
||||
}: {
|
||||
searchParams: { type: string; id: string; secretId?: string };
|
||||
providerUid?: string;
|
||||
}) => {
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
@@ -24,6 +26,7 @@ export const UpdateViaCredentialsForm = ({
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
providerUid={providerUid}
|
||||
onSubmit={handleUpdateCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
|
||||
@@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";
|
||||
|
||||
export const UpdateViaRoleForm = ({
|
||||
searchParams,
|
||||
providerUid,
|
||||
}: {
|
||||
searchParams: { type: string; id: string; secretId?: string };
|
||||
providerUid?: string;
|
||||
}) => {
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
@@ -24,6 +26,7 @@ export const UpdateViaRoleForm = ({
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
providerUid={providerUid}
|
||||
onSubmit={handleUpdateCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
|
||||
@@ -7,8 +7,10 @@ import { BaseCredentialsForm } from "./base-credentials-form";
|
||||
|
||||
export const UpdateViaServiceAccountForm = ({
|
||||
searchParams,
|
||||
providerUid,
|
||||
}: {
|
||||
searchParams: { type: string; id: string; secretId?: string };
|
||||
providerUid?: string;
|
||||
}) => {
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
@@ -24,6 +26,7 @@ export const UpdateViaServiceAccountForm = ({
|
||||
<BaseCredentialsForm
|
||||
providerType={providerType}
|
||||
providerId={providerId}
|
||||
providerUid={providerUid}
|
||||
onSubmit={handleUpdateCredentials}
|
||||
successNavigationUrl={successNavigationUrl}
|
||||
submitButtonText="Next"
|
||||
|
||||
@@ -105,9 +105,7 @@ export const ColumnsRoles: ColumnDef<RolesProps["data"][number]>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
header: () => null,
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
|
||||
@@ -36,8 +36,8 @@ export const DateWithTime = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-1",
|
||||
inline ? "flex-row items-center" : "flex-col",
|
||||
"gap-1",
|
||||
inline ? "inline-flex flex-row items-center" : "flex flex-col",
|
||||
)}
|
||||
>
|
||||
<span className="text-text-neutral-primary text-sm whitespace-nowrap">
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ArrowUpIcon,
|
||||
ChevronsLeftRightIcon,
|
||||
} from "@/components/icons";
|
||||
import { Button } from "@/components/shadcn";
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends HTMLAttributes<HTMLDivElement> {
|
||||
@@ -74,23 +73,25 @@ export const DataTableColumnHeader = <TData, TValue>({
|
||||
);
|
||||
};
|
||||
|
||||
const baseClassName =
|
||||
"text-text-neutral-primary flex h-8 items-center text-left align-middle text-sm font-semibold whitespace-nowrap outline-none -ml-px";
|
||||
|
||||
if (!column.getCanSort()) {
|
||||
return (
|
||||
<div className="text-text-neutral-primary flex items-center justify-between px-0 text-left align-middle text-sm font-semibold whitespace-nowrap outline-none">
|
||||
<div className={baseClassName}>
|
||||
<span className="block break-normal whitespace-nowrap">{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-neutral-primary hover:text-text-neutral-tertiary -ml-3 flex items-center justify-between px-0 text-left align-middle text-sm font-semibold whitespace-nowrap outline-none hover:bg-transparent"
|
||||
<button
|
||||
type="button"
|
||||
className={`${baseClassName} hover:text-text-neutral-tertiary cursor-pointer`}
|
||||
onClick={getToggleSortingHandler}
|
||||
>
|
||||
<span className="block break-normal whitespace-nowrap">{title}</span>
|
||||
{renderSortIcon()}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -85,7 +85,7 @@ const TableHead = forwardRef<
|
||||
className={cn(
|
||||
"bg-bg-neutral-tertiary border-border-neutral-tertiary text-text-neutral-secondary border-y backdrop-blur-[46px]",
|
||||
"h-11 px-1.5 text-left align-middle text-xs font-medium whitespace-nowrap outline-none",
|
||||
"first:rounded-l-full first:border-l first:pl-1",
|
||||
"first:rounded-l-full first:border-l first:pl-3",
|
||||
"last:rounded-r-full last:border-r last:pr-3",
|
||||
"data-[hover=true]:text-foreground-400 data-[focus-visible=true]:outline-focus",
|
||||
"data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-offset-2",
|
||||
|
||||
@@ -9,8 +9,8 @@ import { UserDataWithRoles } from "@/types/users";
|
||||
|
||||
const TenantIdCopy = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap md:flex-col md:items-start md:justify-start">
|
||||
<SnippetChip value={id} />
|
||||
<div className="flex max-w-full min-w-0 items-center gap-2 md:flex-col md:items-start md:justify-start">
|
||||
<SnippetChip value={id} className="max-w-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -48,7 +48,7 @@ export const UserBasicInfoCard = ({
|
||||
</InfoField>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2 overflow-hidden">
|
||||
<InfoField label="Organization ID" variant="transparent">
|
||||
{tenantId ? (
|
||||
<TenantIdCopy id={tenantId} />
|
||||
|
||||
@@ -75,9 +75,7 @@ export const ColumnsUser: ColumnDef<UserProps>[] = [
|
||||
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
header: () => null,
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const roles = row.original.roles;
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/core",
|
||||
"from": "0.3.77",
|
||||
"to": "1.1.4",
|
||||
"from": "1.1.8",
|
||||
"to": "1.1.15",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-15T08:24:46.195Z"
|
||||
"generatedAt": "2026-01-15T08:46:46.283Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -426,10 +426,10 @@
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "langchain",
|
||||
"from": "1.1.4",
|
||||
"to": "1.1.5",
|
||||
"from": "1.1.5",
|
||||
"to": "1.2.10",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-12-15T08:24:46.195Z"
|
||||
"generatedAt": "2026-01-15T08:46:46.283Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -699,9 +699,9 @@
|
||||
"section": "devDependencies",
|
||||
"name": "@types/node",
|
||||
"from": "20.5.7",
|
||||
"to": "20.5.7",
|
||||
"to": "24.10.8",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
"generatedAt": "2026-01-15T08:43:57.254Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
|
||||
+27
-5
@@ -3,8 +3,15 @@
|
||||
* Add entries here for edge cases that heuristics can't handle.
|
||||
*/
|
||||
const SPECIAL_CASES: Record<string, string> = {
|
||||
// Add special cases here if needed, e.g.:
|
||||
// "someweirdcase": "SomeWeirdCase",
|
||||
// Compliance framework acronyms (4+ chars, not caught by length heuristic)
|
||||
gdpr: "GDPR",
|
||||
hipaa: "HIPAA",
|
||||
nist: "NIST",
|
||||
mitre: "MITRE",
|
||||
fedramp: "FedRAMP",
|
||||
ffiec: "FFIEC",
|
||||
kisa: "KISA",
|
||||
cisa: "CISA",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -22,14 +29,29 @@ const SPECIAL_CASES: Record<string, string> = {
|
||||
* - "ec2-imdsv1" -> "EC2 IMDSv1"
|
||||
* - "forensics-ready" -> "Forensics Ready"
|
||||
*/
|
||||
export function getCategoryLabel(id: string): string {
|
||||
/**
|
||||
* Generic label formatter that works with any delimiter.
|
||||
* Use this for formatting IDs into human-readable labels.
|
||||
*
|
||||
* @param id - The ID to format
|
||||
* @param delimiter - The delimiter to split on (default: "-")
|
||||
*/
|
||||
export function formatLabel(id: string, delimiter = "-"): string {
|
||||
return id
|
||||
.split("-")
|
||||
.split(delimiter)
|
||||
.map((word) => formatWord(word))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function formatWord(word: string): string {
|
||||
/**
|
||||
* Converts a category ID to a human-readable label.
|
||||
* Convenience wrapper for formatLabel with "-" delimiter.
|
||||
*/
|
||||
export function getCategoryLabel(id: string): string {
|
||||
return formatLabel(id, "-");
|
||||
}
|
||||
|
||||
export function formatWord(word: string): string {
|
||||
const lowerWord = word.toLowerCase();
|
||||
|
||||
// 1. Check special cases dictionary
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@internationalized/date": "3.10.0",
|
||||
"@langchain/aws": "1.1.0",
|
||||
"@langchain/core": "1.1.8",
|
||||
"@langchain/core": "1.1.15",
|
||||
"@langchain/mcp-adapters": "1.0.3",
|
||||
"@langchain/openai": "1.1.3",
|
||||
"@next/third-parties": "15.5.9",
|
||||
@@ -77,7 +77,7 @@
|
||||
"jose": "5.10.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jwt-decode": "4.0.0",
|
||||
"langchain": "1.1.5",
|
||||
"langchain": "1.2.10",
|
||||
"lucide-react": "0.543.0",
|
||||
"marked": "15.0.12",
|
||||
"nanoid": "5.1.6",
|
||||
@@ -113,7 +113,7 @@
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/d3": "7.4.3",
|
||||
"@types/geojson": "7946.0.16",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/node": "24.10.8",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
|
||||
Generated
+64
-87
@@ -35,16 +35,16 @@ importers:
|
||||
version: 3.10.0
|
||||
'@langchain/aws':
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
version: 1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
'@langchain/core':
|
||||
specifier: 1.1.8
|
||||
version: 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
specifier: 1.1.15
|
||||
version: 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/mcp-adapters':
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))
|
||||
version: 1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))
|
||||
'@langchain/openai':
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
version: 1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
'@next/third-parties':
|
||||
specifier: 15.5.9
|
||||
version: 15.5.9(next@15.5.9(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(react@19.2.2)
|
||||
@@ -172,8 +172,8 @@ importers:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
langchain:
|
||||
specifier: 1.1.5
|
||||
version: 1.1.5(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))
|
||||
specifier: 1.2.10
|
||||
version: 1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))
|
||||
lucide-react:
|
||||
specifier: 0.543.0
|
||||
version: 0.543.0(react@19.2.2)
|
||||
@@ -275,8 +275,8 @@ importers:
|
||||
specifier: 7946.0.16
|
||||
version: 7946.0.16
|
||||
'@types/node':
|
||||
specifier: 20.5.7
|
||||
version: 20.5.7
|
||||
specifier: 24.10.8
|
||||
version: 24.10.8
|
||||
'@types/react':
|
||||
specifier: 19.1.13
|
||||
version: 19.1.13
|
||||
@@ -360,7 +360,7 @@ importers:
|
||||
version: 0.6.14(prettier@3.6.2)
|
||||
shadcn:
|
||||
specifier: 3.4.1
|
||||
version: 3.4.1(@cfworker/json-schema@4.1.1)(@types/node@20.5.7)(typescript@5.5.4)
|
||||
version: 3.4.1(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(typescript@5.5.4)
|
||||
tailwind-variants:
|
||||
specifier: 0.1.20
|
||||
version: 0.1.20(tailwindcss@4.1.13)
|
||||
@@ -1711,8 +1711,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.0.0
|
||||
|
||||
'@langchain/core@1.1.8':
|
||||
resolution: {integrity: sha512-kIUidOgc0ZdyXo4Ahn9Zas+OayqOfk4ZoKPi7XaDipNSWSApc2+QK5BVcjvwtzxstsNOrmXJiJWEN6WPF/MvAw==}
|
||||
'@langchain/core@1.1.15':
|
||||
resolution: {integrity: sha512-b8RN5DkWAmDAlMu/UpTZEluYwCLpm63PPWniRKlE8ie3KkkE7IuMQ38pf4kV1iaiI+d99BEQa2vafQHfCujsRA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@langchain/langgraph-checkpoint@1.0.0':
|
||||
@@ -4337,8 +4337,8 @@ packages:
|
||||
'@types/mysql@2.15.27':
|
||||
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
|
||||
|
||||
'@types/node@20.5.7':
|
||||
resolution: {integrity: sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==}
|
||||
'@types/node@24.10.8':
|
||||
resolution: {integrity: sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==}
|
||||
|
||||
'@types/pg-pool@2.0.6':
|
||||
resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==}
|
||||
@@ -6401,33 +6401,16 @@ packages:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
langchain@1.1.5:
|
||||
resolution: {integrity: sha512-tmJHdCsi4AQLEWDeTm9QTWgdwYgIaA4kfp14KFw6e1sUPxjsoHqdFqdf1ZJZxhs1h/n+hpIr3NBfGNBQnWxWEQ==}
|
||||
langchain@1.2.10:
|
||||
resolution: {integrity: sha512-9uVxOJE/RTECvNutQfOLwH7f6R9mcq0G/IMHwA2eptDA86R/Yz2zWMz4vARVFPxPrdSJ9nJFDPAqRQlRFwdHBw==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@langchain/core': 1.1.4
|
||||
'@langchain/core': 1.1.15
|
||||
|
||||
langium@3.3.1:
|
||||
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
langsmith@0.3.87:
|
||||
resolution: {integrity: sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': '*'
|
||||
'@opentelemetry/exporter-trace-otlp-proto': '*'
|
||||
'@opentelemetry/sdk-trace-base': '*'
|
||||
openai: '*'
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@opentelemetry/exporter-trace-otlp-proto':
|
||||
optional: true
|
||||
'@opentelemetry/sdk-trace-base':
|
||||
optional: true
|
||||
openai:
|
||||
optional: true
|
||||
|
||||
langsmith@0.4.2:
|
||||
resolution: {integrity: sha512-BvBeFgSmR9esl8x5wsiDlALiHKKPybw2wE2Hh6x1tgSZki46H9c9KI9/06LARbPhyyDu/TZU7exfg6fnhdj1Qg==}
|
||||
peerDependencies:
|
||||
@@ -8155,6 +8138,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
unicorn-magic@0.3.0:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10596,31 +10582,31 @@ snapshots:
|
||||
|
||||
'@inquirer/ansi@1.0.2': {}
|
||||
|
||||
'@inquirer/confirm@5.1.21(@types/node@20.5.7)':
|
||||
'@inquirer/confirm@5.1.21(@types/node@24.10.8)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.3.2(@types/node@20.5.7)
|
||||
'@inquirer/type': 3.0.10(@types/node@20.5.7)
|
||||
'@inquirer/core': 10.3.2(@types/node@24.10.8)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.10.8)
|
||||
optionalDependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@inquirer/core@10.3.2(@types/node@20.5.7)':
|
||||
'@inquirer/core@10.3.2(@types/node@24.10.8)':
|
||||
dependencies:
|
||||
'@inquirer/ansi': 1.0.2
|
||||
'@inquirer/figures': 1.0.15
|
||||
'@inquirer/type': 3.0.10(@types/node@20.5.7)
|
||||
'@inquirer/type': 3.0.10(@types/node@24.10.8)
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.3
|
||||
optionalDependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@inquirer/figures@1.0.15': {}
|
||||
|
||||
'@inquirer/type@3.0.10(@types/node@20.5.7)':
|
||||
'@inquirer/type@3.0.10(@types/node@24.10.8)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@internationalized/date@3.10.0':
|
||||
dependencies:
|
||||
@@ -10682,17 +10668,17 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@langchain/aws@1.1.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
'@langchain/aws@1.1.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
dependencies:
|
||||
'@aws-sdk/client-bedrock-agent-runtime': 3.948.0
|
||||
'@aws-sdk/client-bedrock-runtime': 3.948.0
|
||||
'@aws-sdk/client-kendra': 3.948.0
|
||||
'@aws-sdk/credential-provider-node': 3.948.0
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))':
|
||||
'@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))':
|
||||
dependencies:
|
||||
'@cfworker/json-schema': 4.1.1
|
||||
ansi-styles: 5.2.0
|
||||
@@ -10710,26 +10696,26 @@ snapshots:
|
||||
- '@opentelemetry/sdk-trace-base'
|
||||
- openai
|
||||
|
||||
'@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
'@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
uuid: 10.0.0
|
||||
|
||||
'@langchain/langgraph-sdk@1.2.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
'@langchain/langgraph-sdk@1.2.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
dependencies:
|
||||
p-queue: 6.6.2
|
||||
p-retry: 4.6.2
|
||||
uuid: 9.0.1
|
||||
optionalDependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
|
||||
'@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)':
|
||||
'@langchain/langgraph@1.0.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)':
|
||||
dependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
'@langchain/langgraph-sdk': 1.2.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
'@langchain/langgraph-sdk': 1.2.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
uuid: 10.0.0
|
||||
zod: 4.1.11
|
||||
optionalDependencies:
|
||||
@@ -10738,10 +10724,10 @@ snapshots:
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
'@langchain/mcp-adapters@1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))':
|
||||
'@langchain/mcp-adapters@1.0.3(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@langchain/langgraph@1.0.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph': 1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph': 1.0.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)
|
||||
'@modelcontextprotocol/sdk': 1.24.3(@cfworker/json-schema@4.1.1)(zod@4.1.11)
|
||||
debug: 4.4.3
|
||||
zod: 4.1.11
|
||||
@@ -10751,9 +10737,9 @@ snapshots:
|
||||
- '@cfworker/json-schema'
|
||||
- supports-color
|
||||
|
||||
'@langchain/openai@1.1.3(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
'@langchain/openai@1.1.3(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))':
|
||||
dependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
js-tiktoken: 1.0.21
|
||||
openai: 6.10.0(zod@4.1.11)
|
||||
zod: 4.1.11
|
||||
@@ -13905,7 +13891,7 @@ snapshots:
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
@@ -14066,9 +14052,11 @@ snapshots:
|
||||
|
||||
'@types/mysql@2.15.27':
|
||||
dependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@types/node@20.5.7': {}
|
||||
'@types/node@24.10.8':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/pg-pool@2.0.6':
|
||||
dependencies:
|
||||
@@ -14076,7 +14064,7 @@ snapshots:
|
||||
|
||||
'@types/pg@8.15.6':
|
||||
dependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
|
||||
@@ -14094,7 +14082,7 @@ snapshots:
|
||||
|
||||
'@types/tedious@4.0.14':
|
||||
dependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
|
||||
'@types/topojson-client@3.1.5':
|
||||
dependencies:
|
||||
@@ -16295,7 +16283,7 @@ snapshots:
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 20.5.7
|
||||
'@types/node': 24.10.8
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
@@ -16364,12 +16352,12 @@ snapshots:
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
langchain@1.1.5(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)):
|
||||
langchain@1.2.10(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11)):
|
||||
dependencies:
|
||||
'@langchain/core': 1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph': 1.0.4(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)
|
||||
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
langsmith: 0.3.87(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/core': 1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
'@langchain/langgraph': 1.0.4(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(zod-to-json-schema@3.25.0(zod@4.1.11))(zod@4.1.11)
|
||||
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.15(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)))
|
||||
langsmith: 0.4.2(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11))
|
||||
uuid: 10.0.0
|
||||
zod: 4.1.11
|
||||
transitivePeerDependencies:
|
||||
@@ -16389,19 +16377,6 @@ snapshots:
|
||||
vscode-languageserver-textdocument: 1.0.12
|
||||
vscode-uri: 3.0.8
|
||||
|
||||
langsmith@0.3.87(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)):
|
||||
dependencies:
|
||||
'@types/uuid': 10.0.0
|
||||
chalk: 4.1.2
|
||||
console-table-printer: 2.15.0
|
||||
p-queue: 6.6.2
|
||||
semver: 7.7.3
|
||||
uuid: 10.0.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
openai: 6.10.0(zod@4.1.11)
|
||||
|
||||
langsmith@0.4.2(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(zod@4.1.11)):
|
||||
dependencies:
|
||||
'@types/uuid': 10.0.0
|
||||
@@ -17057,9 +17032,9 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msw@2.12.4(@types/node@20.5.7)(typescript@5.5.4):
|
||||
msw@2.12.4(@types/node@24.10.8)(typescript@5.5.4):
|
||||
dependencies:
|
||||
'@inquirer/confirm': 5.1.21(@types/node@20.5.7)
|
||||
'@inquirer/confirm': 5.1.21(@types/node@24.10.8)
|
||||
'@mswjs/interceptors': 0.40.0
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@types/statuses': 2.0.6
|
||||
@@ -18016,7 +17991,7 @@ snapshots:
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shadcn@3.4.1(@cfworker/json-schema@4.1.1)(@types/node@20.5.7)(typescript@5.5.4):
|
||||
shadcn@3.4.1(@cfworker/json-schema@4.1.1)(@types/node@24.10.8)(typescript@5.5.4):
|
||||
dependencies:
|
||||
'@antfu/ni': 25.0.0
|
||||
'@babel/core': 7.28.5
|
||||
@@ -18037,7 +18012,7 @@ snapshots:
|
||||
fuzzysort: 3.1.0
|
||||
https-proxy-agent: 7.0.6
|
||||
kleur: 4.1.5
|
||||
msw: 2.12.4(@types/node@20.5.7)(typescript@5.5.4)
|
||||
msw: 2.12.4(@types/node@24.10.8)(typescript@5.5.4)
|
||||
node-fetch: 3.3.2
|
||||
ora: 8.2.0
|
||||
postcss: 8.5.6
|
||||
@@ -18559,6 +18534,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unified@11.0.5:
|
||||
|
||||
@@ -607,18 +607,22 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
|
||||
// Fallback logic: try finding any common primary action buttons in expected order
|
||||
const candidates: Array<{ name: string | RegExp }> = [
|
||||
{ name: "Next" }, // Try the "Next" button
|
||||
{ name: "Save" }, // Try the "Save" button
|
||||
const candidates: Array<{ name: string | RegExp; exact?: boolean }> = [
|
||||
{ name: "Next", exact: true }, // Try the "Next" button (exact match to avoid Next.js dev tools)
|
||||
{ name: "Save", exact: true }, // Try the "Save" button
|
||||
{ name: "Launch scan" }, // Try the "Launch scan" button
|
||||
{ name: /Continue|Proceed/i }, // Try "Continue" or "Proceed" (case-insensitive)
|
||||
];
|
||||
|
||||
// Try each candidate name and click it if found
|
||||
for (const candidate of candidates) {
|
||||
const btn = this.page.getByRole("button", {
|
||||
name: candidate.name,
|
||||
});
|
||||
// Exclude Next.js dev tools button by filtering out buttons with aria-haspopup attribute
|
||||
const btn = this.page
|
||||
.getByRole("button", {
|
||||
name: candidate.name,
|
||||
exact: candidate.exact,
|
||||
})
|
||||
.and(this.page.locator(":not([aria-haspopup])"));
|
||||
|
||||
if (await btn.count()) {
|
||||
await btn.click();
|
||||
@@ -847,7 +851,7 @@ export class ProvidersPage extends BasePage {
|
||||
}
|
||||
|
||||
async verifyOCICredentialsPageLoaded(): Promise<void> {
|
||||
// Verify the OCI credentials page is loaded
|
||||
// Verify the OCI credentials page is loaded (add flow - all fields visible)
|
||||
|
||||
await this.verifyPageHasProwlerTitle();
|
||||
await expect(this.ociTenancyIdInput).toBeVisible();
|
||||
@@ -857,6 +861,17 @@ export class ProvidersPage extends BasePage {
|
||||
await expect(this.ociRegionInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOCIUpdateCredentialsPageLoaded(): Promise<void> {
|
||||
// Verify the OCI update credentials page is loaded
|
||||
// Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID)
|
||||
|
||||
await this.verifyPageHasProwlerTitle();
|
||||
await expect(this.ociUserIdInput).toBeVisible();
|
||||
await expect(this.ociFingerprintInput).toBeVisible();
|
||||
await expect(this.ociKeyContentInput).toBeVisible();
|
||||
await expect(this.ociRegionInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
// Verify the providers page is loaded
|
||||
|
||||
@@ -995,4 +1010,42 @@ export class ProvidersPage extends BasePage {
|
||||
throw new Error(`Invalid authentication method: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
async clickProviderRowActions(providerUid: string): Promise<void> {
|
||||
// Click the actions dropdown for a specific provider row
|
||||
const row = this.providersTable.locator("tbody tr", {
|
||||
hasText: providerUid,
|
||||
});
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
// Click the dropdown trigger - it's the last button in the row (after the copy button)
|
||||
const actionsButton = row.locator("button").last();
|
||||
await actionsButton.click();
|
||||
}
|
||||
|
||||
async clickUpdateCredentials(providerUid: string): Promise<void> {
|
||||
// Click update credentials for a specific provider
|
||||
await this.clickProviderRowActions(providerUid);
|
||||
|
||||
// Wait for dropdown menu to stabilize and click Update Credentials
|
||||
const updateCredentialsOption = this.page.getByRole("menuitem", {
|
||||
name: /Update Credentials/i,
|
||||
});
|
||||
await expect(updateCredentialsOption).toBeVisible();
|
||||
// Wait a bit for the menu to stabilize before clicking
|
||||
await this.page.waitForTimeout(100);
|
||||
await updateCredentialsOption.click({ force: true });
|
||||
}
|
||||
|
||||
async verifyUpdateCredentialsPageLoaded(): Promise<void> {
|
||||
// Verify the update credentials page is loaded
|
||||
await this.verifyPageHasProwlerTitle();
|
||||
await expect(this.page).toHaveURL(/\/providers\/update-credentials/);
|
||||
}
|
||||
|
||||
async verifyTestConnectionPageLoaded(): Promise<void> {
|
||||
// Verify the test connection page is loaded
|
||||
await this.verifyPageHasProwlerTitle();
|
||||
await expect(this.page).toHaveURL(/\/providers\/test-connection/);
|
||||
}
|
||||
}
|
||||
@@ -708,3 +708,61 @@
|
||||
- Provider cleanup performed before each test to ensure clean state
|
||||
- Requires valid OCI account with API Key set up
|
||||
- API Key credential type is automatically used for OCI providers
|
||||
|
||||
---
|
||||
|
||||
## Test Case: `PROVIDER-E2E-013` - Update OCI Provider Credentials
|
||||
|
||||
**Priority:** `normal`
|
||||
|
||||
**Tags:**
|
||||
|
||||
- type → @e2e, @serial
|
||||
- feature → @providers
|
||||
- provider → @oci
|
||||
|
||||
**Description/Objective:** Validates the complete flow of updating credentials for an existing OCI provider. This test verifies that the provider UID is correctly passed to the update credentials form, which is required for OCI credential validation.
|
||||
|
||||
**Preconditions:**
|
||||
|
||||
- Admin user authentication required (admin.auth.setup setup)
|
||||
- Environment variables configured: E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, E2E_OCI_REGION
|
||||
- An OCI provider with the specified Tenancy ID must already exist (run PROVIDER-E2E-012 first)
|
||||
- This test must be run serially and never in parallel with other tests
|
||||
|
||||
### Flow Steps:
|
||||
|
||||
1. Navigate to providers page
|
||||
2. Verify OCI provider exists in the table
|
||||
3. Click row actions menu for the OCI provider
|
||||
4. Click "Update Credentials" option
|
||||
5. Verify update credentials page is loaded
|
||||
6. Verify OCI credentials form fields are visible (confirms providerUid is loaded)
|
||||
7. Fill OCI credentials (user ID, fingerprint, key content, region)
|
||||
8. Click Next to submit
|
||||
9. Verify successful navigation to test connection page
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- Update credentials page loads successfully
|
||||
- OCI credentials form is displayed with all required fields
|
||||
- Provider UID is correctly passed to the form (hidden field populated)
|
||||
- Credentials can be updated and submitted
|
||||
- User is redirected to test connection page after successful update
|
||||
|
||||
### Key verification points:
|
||||
|
||||
- Provider page loads correctly
|
||||
- OCI provider row is visible in providers table
|
||||
- Row actions dropdown opens and displays "Update Credentials" option
|
||||
- Update credentials page URL contains correct parameters
|
||||
- OCI credentials form displays all fields (tenancy ID, user ID, fingerprint, key content, region)
|
||||
- Form submission succeeds (no silent failures due to missing provider UID)
|
||||
- Successful redirect to test connection page
|
||||
|
||||
### Notes:
|
||||
|
||||
- Test uses same environment variables as PROVIDER-E2E-012 (add OCI provider)
|
||||
- Requires PROVIDER-E2E-012 to be run first to create the OCI provider
|
||||
- This test validates the fix for OCI update credentials form failing silently due to missing provider UID
|
||||
- The provider UID is required for OCI credential validation (tenancy field auto-populated from UID)
|
||||
@@ -1139,3 +1139,87 @@ test.describe("Add Provider", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Update Provider Credentials", () => {
|
||||
test.describe.serial("Update OCI Provider Credentials", () => {
|
||||
let providersPage: ProvidersPage;
|
||||
|
||||
// Test data from environment variables (same as add OCI provider test)
|
||||
const tenancyId = process.env.E2E_OCI_TENANCY_ID;
|
||||
const userId = process.env.E2E_OCI_USER_ID;
|
||||
const fingerprint = process.env.E2E_OCI_FINGERPRINT;
|
||||
const keyContent = process.env.E2E_OCI_KEY_CONTENT;
|
||||
const region = process.env.E2E_OCI_REGION;
|
||||
|
||||
// Validate required environment variables
|
||||
if (!tenancyId || !userId || !fingerprint || !keyContent || !region) {
|
||||
throw new Error(
|
||||
"E2E_OCI_TENANCY_ID, E2E_OCI_USER_ID, E2E_OCI_FINGERPRINT, E2E_OCI_KEY_CONTENT, and E2E_OCI_REGION environment variables are not set",
|
||||
);
|
||||
}
|
||||
|
||||
// Setup before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
providersPage = new ProvidersPage(page);
|
||||
});
|
||||
|
||||
// Use admin authentication for provider management
|
||||
test.use({ storageState: "playwright/.auth/admin_user.json" });
|
||||
|
||||
test(
|
||||
"should update OCI provider credentials successfully",
|
||||
{
|
||||
tag: [
|
||||
"@e2e",
|
||||
"@providers",
|
||||
"@oci",
|
||||
"@serial",
|
||||
"@PROVIDER-E2E-013",
|
||||
],
|
||||
},
|
||||
async () => {
|
||||
// Prepare updated credentials
|
||||
const ociCredentials: OCIProviderCredential = {
|
||||
type: OCI_CREDENTIAL_OPTIONS.OCI_API_KEY,
|
||||
tenancyId: tenancyId,
|
||||
userId: userId,
|
||||
fingerprint: fingerprint,
|
||||
keyContent: keyContent,
|
||||
region: region,
|
||||
};
|
||||
|
||||
// Navigate to providers page
|
||||
await providersPage.goto();
|
||||
await providersPage.verifyPageLoaded();
|
||||
|
||||
// Verify OCI provider exists in the table
|
||||
const providerExists =
|
||||
await providersPage.verifySingleRowForProviderUID(tenancyId);
|
||||
if (!providerExists) {
|
||||
throw new Error(
|
||||
`OCI provider with tenancy ID ${tenancyId} not found. Run the add OCI provider test first.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Click update credentials for the OCI provider
|
||||
await providersPage.clickUpdateCredentials(tenancyId);
|
||||
|
||||
// Verify update credentials page is loaded
|
||||
await providersPage.verifyUpdateCredentialsPageLoaded();
|
||||
|
||||
// Verify OCI credentials form fields are visible (confirms providerUid is loaded)
|
||||
// Note: Tenancy OCID is hidden in update flow (auto-populated from provider UID)
|
||||
await providersPage.verifyOCIUpdateCredentialsPageLoaded();
|
||||
|
||||
// Fill updated credentials
|
||||
await providersPage.fillOCICredentials(ociCredentials);
|
||||
|
||||
// Click Next to submit
|
||||
await providersPage.clickNext();
|
||||
|
||||
// Verify successful navigation to test connection page
|
||||
await providersPage.verifyTestConnectionPageLoaded();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user