mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
fix(ui): pre-release fixes and improvements (#9278)
This commit is contained in:
@@ -8,6 +8,8 @@ import { LighthouseIcon } from "@/components/icons/Icons";
|
||||
import { Chat } from "@/components/lighthouse";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AIChatbot() {
|
||||
const hasConfig = await isLighthouseConfigured();
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) {
|
||||
: [];
|
||||
const selectedIds = current ? current.split(",").filter(Boolean) : [];
|
||||
const visibleProviders = providers
|
||||
.filter((p) => p.attributes.connection?.connected)
|
||||
// .filter((p) => p.attributes.connection?.connected)
|
||||
.filter((p) =>
|
||||
selectedTypesList.length > 0
|
||||
? selectedTypesList.includes(p.attributes.provider)
|
||||
|
||||
@@ -132,7 +132,7 @@ export const ProviderTypeSelector = ({
|
||||
const availableTypes = Array.from(
|
||||
new Set(
|
||||
providers
|
||||
.filter((p) => p.attributes.connection?.connected)
|
||||
// .filter((p) => p.attributes.connection?.connected)
|
||||
.map((p) => p.attributes.provider),
|
||||
),
|
||||
) as ProviderType[];
|
||||
|
||||
@@ -21,10 +21,10 @@ export const ThreatScoreSSR = async ({
|
||||
const snapshot = threatScoreData.data[0];
|
||||
const attributes = snapshot.attributes;
|
||||
|
||||
// Parse score from decimal string to number and round to integer
|
||||
const score = Math.round(parseFloat(attributes.overall_score));
|
||||
// Parse score from decimal string to number
|
||||
const score = parseFloat(attributes.overall_score);
|
||||
const scoreDelta = attributes.score_delta
|
||||
? Math.round(parseFloat(attributes.score_delta))
|
||||
? parseFloat(attributes.score_delta)
|
||||
: null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -66,14 +66,11 @@ function convertSectionScoresToTooltipData(
|
||||
if (!sectionScores) return [];
|
||||
|
||||
return Object.entries(sectionScores).map(([name, value]) => {
|
||||
// Round to nearest integer
|
||||
const roundedValue = Math.round(value);
|
||||
|
||||
// Determine color based on the same ranges as THREAT_LEVEL_CONFIG
|
||||
const threatLevel = getThreatLevel(roundedValue);
|
||||
const threatLevel = getThreatLevel(value);
|
||||
const color = THREAT_LEVEL_CONFIG[threatLevel].chartColor;
|
||||
|
||||
return { name, value: roundedValue, color };
|
||||
return { name, value, color };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,8 +159,12 @@ export function ThreatScore({
|
||||
{scoreDelta !== undefined &&
|
||||
scoreDelta !== null &&
|
||||
scoreDelta !== 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<ThumbsUp size={14} className="flex-shrink-0" />
|
||||
<div className="flex items-start gap-1.5">
|
||||
<ThumbsUp
|
||||
size={16}
|
||||
className="mt-0.5 shrink-0"
|
||||
style={{ minWidth: "16px", minHeight: "16px" }}
|
||||
/>
|
||||
<p>
|
||||
Threat score has{" "}
|
||||
{scoreDelta > 0 ? "improved" : "decreased"} by{" "}
|
||||
@@ -174,10 +175,11 @@ export function ThreatScore({
|
||||
|
||||
{/* Gaps Message */}
|
||||
{gaps.length > 0 && (
|
||||
<div className="flex items-start gap-1">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<MessageCircleWarning
|
||||
size={14}
|
||||
className="mt-1 flex-shrink-0"
|
||||
size={16}
|
||||
className="mt-0.5 shrink-0"
|
||||
style={{ minWidth: "16px", minHeight: "16px" }}
|
||||
/>
|
||||
<p>
|
||||
Major gaps include {gaps.slice(0, 2).join(", ")}
|
||||
|
||||
@@ -10,18 +10,24 @@ interface AuthLayoutProps {
|
||||
|
||||
export const AuthLayout = ({ title, children }: AuthLayoutProps) => {
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen">
|
||||
<div className="relative flex w-full items-center justify-center lg:w-full">
|
||||
<div className="relative flex min-h-screen w-full overflow-x-hidden overflow-y-auto">
|
||||
<div className="relative flex w-full flex-col items-center justify-center px-4 py-32">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute h-full w-full bg-[radial-gradient(#6af400_1px,transparent_1px)] mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)] bg-size-[16px_16px]"></div>
|
||||
<div
|
||||
className="absolute inset-0 mask-[radial-gradient(ellipse_50%_50%_at_50%_50%,#000_10%,transparent_80%)] bg-size-[16px_16px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(var(--bg-button-primary) 1px, transparent 1px)",
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Prowler Logo */}
|
||||
<div className="relative z-10 mb-8 flex w-full max-w-[300px]">
|
||||
<ProwlerExtended width={300} className="h-auto w-full" />
|
||||
</div>
|
||||
|
||||
{/* Auth Form Container */}
|
||||
<div className="rounded-large border-divider shadow-small dark:bg-background/85 relative z-10 flex w-full max-w-sm flex-col gap-4 border bg-white/90 px-8 py-10 md:max-w-md">
|
||||
{/* Prowler Logo */}
|
||||
<div className="absolute -top-[100px] left-1/2 z-10 flex h-fit w-fit -translate-x-1/2">
|
||||
<ProwlerExtended width={300} />
|
||||
</div>
|
||||
|
||||
{/* Header with Title and Theme Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-2 text-xl font-medium">{title}</p>
|
||||
|
||||
@@ -58,21 +58,17 @@ export const PasswordRequirementsMessage = ({
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className={`rounded-xl border p-3 ${
|
||||
allRequirementsMet
|
||||
? "border-system-success bg-system-success/10"
|
||||
: "border-red-200 bg-red-50"
|
||||
}`}
|
||||
className={`bg-bg-neutral-primary rounded-xl border p-3 ${allRequirementsMet ? "border-bg-pass" : "border-bg-fail"}`}
|
||||
role="region"
|
||||
aria-label="Password requirements status"
|
||||
>
|
||||
{allRequirementsMet ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle
|
||||
className="text-system-success h-4 w-4 shrink-0"
|
||||
className="text-text-success h-4 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-system-success text-xs leading-tight font-medium">
|
||||
<p className="text-text-neutral-primary text-xs leading-tight font-medium">
|
||||
Password meets all requirements
|
||||
</p>
|
||||
</div>
|
||||
@@ -80,10 +76,10 @@ export const PasswordRequirementsMessage = ({
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle
|
||||
className="h-4 w-4 shrink-0 text-red-600"
|
||||
className="text-text-error h-4 w-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-xs leading-tight font-medium text-red-700">
|
||||
<p className="text-text-neutral-primary text-xs leading-tight font-medium">
|
||||
Password must include:
|
||||
</p>
|
||||
</div>
|
||||
@@ -99,12 +95,12 @@ export const PasswordRequirementsMessage = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`h-2 w-2 shrink-0 rounded-full ${
|
||||
req.isMet ? "bg-system-success" : "bg-red-400"
|
||||
req.isMet ? "bg-text-success" : "bg-text-error"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className={`${req.isMet ? "text-system-success" : "text-red-700"}`}
|
||||
className="text-text-success-primary"
|
||||
aria-label={`${req.label} ${req.isMet ? "satisfied" : "required"}`}
|
||||
>
|
||||
{req.label}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Checkbox } from "@heroui/checkbox";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import { createNewUser } from "@/actions/auth";
|
||||
import { AuthDivider } from "@/components/auth/oss/auth-divider";
|
||||
@@ -62,6 +62,12 @@ export const SignUpForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const passwordValue = useWatch({
|
||||
control: form.control,
|
||||
name: "password",
|
||||
defaultValue: "",
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmit = async (data: SignUpFormData) => {
|
||||
@@ -152,9 +158,7 @@ export const SignUpForm = ({
|
||||
showFormMessage
|
||||
/>
|
||||
<CustomInput control={form.control} name="password" password />
|
||||
<PasswordRequirementsMessage
|
||||
password={form.watch("password") || ""}
|
||||
/>
|
||||
<PasswordRequirementsMessage password={passwordValue || ""} />
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { Accordion, AccordionItemProps } from "@/components/ui";
|
||||
|
||||
export const ClientAccordionWrapper = ({
|
||||
@@ -59,12 +60,15 @@ export const ClientAccordionWrapper = ({
|
||||
<div>
|
||||
{!hideExpandButton && (
|
||||
<div className="text-text-neutral-tertiary hover:text-text-neutral-primary mt-[-16px] flex justify-end text-xs font-medium transition-colors">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleToggleExpand}
|
||||
aria-label={isExpanded ? "Collapse all" : "Expand all"}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mb-1"
|
||||
>
|
||||
{isExpanded ? "Collapse all" : "Expand all"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Accordion
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Divider } from "@heroui/divider";
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
interface ComplianceScanInfoProps {
|
||||
@@ -20,15 +20,16 @@ interface ComplianceScanInfoProps {
|
||||
|
||||
export const ComplianceScanInfo = ({ scan }: ComplianceScanInfoProps) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<EntityInfoShort
|
||||
cloudProvider={scan.providerInfo.provider}
|
||||
entityAlias={scan.providerInfo.alias}
|
||||
entityId={scan.providerInfo.uid}
|
||||
hideCopyButton
|
||||
snippetWidth="max-w-[100px]"
|
||||
/>
|
||||
<Divider orientation="vertical" className="h-6" />
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex shrink-0 items-center">
|
||||
<EntityInfo
|
||||
cloudProvider={scan.providerInfo.provider}
|
||||
entityAlias={scan.providerInfo.alias}
|
||||
entityId={scan.providerInfo.uid}
|
||||
showCopyAction={false}
|
||||
/>
|
||||
</div>
|
||||
<Divider orientation="vertical" className="h-8" />
|
||||
<div className="flex flex-col items-start whitespace-nowrap">
|
||||
<Tooltip
|
||||
content={scan.attributes.name || "- -"}
|
||||
|
||||
@@ -115,7 +115,7 @@ export const ThreatScoreBadge = ({
|
||||
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className={`text-2xl font-bold ${getTextColor()}`}>
|
||||
{score.toFixed(1)}%
|
||||
{score}%
|
||||
</span>
|
||||
<Progress
|
||||
aria-label="ThreatScore progress"
|
||||
|
||||
@@ -5,14 +5,7 @@ import { Select, SelectItem } from "@heroui/select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { Selection } from "@react-types/shared";
|
||||
import { Search, Send } from "lucide-react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -49,6 +42,15 @@ const sendToJiraSchema = z.object({
|
||||
|
||||
type SendToJiraFormData = z.infer<typeof sendToJiraSchema>;
|
||||
|
||||
const selectorClassNames = {
|
||||
trigger: "min-h-12",
|
||||
popoverContent: "bg-bg-neutral-secondary",
|
||||
listboxWrapper: "max-h-[300px] bg-bg-neutral-secondary",
|
||||
listbox: "gap-0",
|
||||
label: "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!",
|
||||
value: "text-text-neutral-secondary text-small",
|
||||
};
|
||||
|
||||
// The commented code is related to issue types, which are not required for the first implementation, but will be used in the future
|
||||
export const SendToJiraModal = ({
|
||||
isOpen,
|
||||
@@ -75,9 +77,8 @@ export const SendToJiraModal = ({
|
||||
const selectedIntegration = form.watch("integration");
|
||||
// const selectedProject = form.watch("project");
|
||||
|
||||
const hasConnectedIntegration = useMemo(
|
||||
() => integrations.some((i) => i.attributes.connected === true),
|
||||
[integrations],
|
||||
const hasConnectedIntegration = integrations.some(
|
||||
(i) => i.attributes.connected === true,
|
||||
);
|
||||
|
||||
const getSelectedValue = (keys: Selection): string => {
|
||||
@@ -91,37 +92,39 @@ export const SendToJiraModal = ({
|
||||
onOpenChange(next);
|
||||
};
|
||||
|
||||
const fetchJiraIntegrations = useCallback(async () => {
|
||||
setIsFetchingIntegrations(true);
|
||||
|
||||
try {
|
||||
const result = await getJiraIntegrations();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Unable to fetch Jira integrations");
|
||||
}
|
||||
setIntegrations(result.data);
|
||||
// Auto-select if only one integration
|
||||
if (result.data.length === 1) {
|
||||
form.setValue("integration", result.data[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: "Failed to load Jira integrations";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to load integrations",
|
||||
description: message,
|
||||
});
|
||||
} finally {
|
||||
setIsFetchingIntegrations(false);
|
||||
}
|
||||
}, [form, toast]);
|
||||
|
||||
// Fetch Jira integrations when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const fetchJiraIntegrations = async () => {
|
||||
setIsFetchingIntegrations(true);
|
||||
|
||||
try {
|
||||
const result = await getJiraIntegrations();
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
result.error || "Unable to fetch Jira integrations",
|
||||
);
|
||||
}
|
||||
setIntegrations(result.data);
|
||||
// Auto-select if only one integration
|
||||
if (result.data.length === 1) {
|
||||
form.setValue("integration", result.data[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: "Failed to load Jira integrations";
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to load integrations",
|
||||
description: message,
|
||||
});
|
||||
} finally {
|
||||
setIsFetchingIntegrations(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchJiraIntegrations();
|
||||
} else {
|
||||
// Reset form when modal closes
|
||||
@@ -129,7 +132,7 @@ export const SendToJiraModal = ({
|
||||
setSearchProjectValue("");
|
||||
// setSearchIssueTypeValue("");
|
||||
}
|
||||
}, [isOpen, form, fetchJiraIntegrations]);
|
||||
}, [isOpen, form, toast]);
|
||||
|
||||
const handleSubmit = async (data: SendToJiraFormData) => {
|
||||
// Close modal immediately; continue processing in background
|
||||
@@ -179,19 +182,18 @@ export const SendToJiraModal = ({
|
||||
(i) => i.id === selectedIntegration,
|
||||
);
|
||||
|
||||
const projects: Record<string, string> = useMemo(
|
||||
() =>
|
||||
selectedIntegrationData?.attributes.configuration.projects ??
|
||||
({} as Record<string, string>),
|
||||
[selectedIntegrationData],
|
||||
);
|
||||
const projects: Record<string, string> =
|
||||
selectedIntegrationData?.attributes.configuration.projects ??
|
||||
({} as Record<string, string>);
|
||||
|
||||
const projectEntries = Object.entries(projects);
|
||||
const shouldShowProjectSearch = projectEntries.length > 5;
|
||||
// const issueTypes: string[] =
|
||||
// selectedIntegrationData?.attributes.configuration.issue_types ||
|
||||
// ([] as string[]);
|
||||
|
||||
// Filter projects based on search
|
||||
const filteredProjects = useMemo(() => {
|
||||
const projectEntries = Object.entries(projects);
|
||||
const filteredProjects = (() => {
|
||||
if (!searchProjectValue) return projectEntries;
|
||||
|
||||
const lowerSearch = searchProjectValue.toLowerCase();
|
||||
@@ -200,7 +202,7 @@ export const SendToJiraModal = ({
|
||||
key.toLowerCase().includes(lowerSearch) ||
|
||||
name.toLowerCase().includes(lowerSearch),
|
||||
);
|
||||
}, [projects, searchProjectValue]);
|
||||
})();
|
||||
|
||||
// Filter issue types based on search
|
||||
// const filteredIssueTypes = useMemo(() => {
|
||||
@@ -257,13 +259,7 @@ export const SendToJiraModal = ({
|
||||
isDisabled={isFetchingIntegrations}
|
||||
isInvalid={!!form.formState.errors.integration}
|
||||
startContent={<JiraIcon size={16} />}
|
||||
classNames={{
|
||||
trigger: "min-h-12",
|
||||
popoverContent: "dark:bg-gray-800",
|
||||
label:
|
||||
"tracking-tight font-light !text-default-500 text-xs z-0!",
|
||||
value: "text-default-500 text-small dark:text-gray-300",
|
||||
}}
|
||||
classNames={selectorClassNames}
|
||||
>
|
||||
{integrations.map((integration) => (
|
||||
<SelectItem
|
||||
@@ -312,35 +308,28 @@ export const SendToJiraModal = ({
|
||||
variant="bordered"
|
||||
labelPlacement="inside"
|
||||
isInvalid={!!form.formState.errors.project}
|
||||
classNames={{
|
||||
trigger: "min-h-12",
|
||||
popoverContent: "dark:bg-gray-800",
|
||||
listboxWrapper: "max-h-[300px] dark:bg-gray-800",
|
||||
label:
|
||||
"tracking-tight font-light !text-default-500 text-xs z-0!",
|
||||
value: "text-default-500 text-small dark:text-gray-300",
|
||||
}}
|
||||
classNames={selectorClassNames}
|
||||
listboxProps={{
|
||||
topContent:
|
||||
filteredProjects.length > 5 ? (
|
||||
<div className="bg-content1 sticky top-0 z-10 py-2 dark:bg-gray-800">
|
||||
<Input
|
||||
isClearable
|
||||
placeholder="Search projects..."
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
startContent={<Search size={16} />}
|
||||
value={searchProjectValue}
|
||||
onValueChange={setSearchProjectValue}
|
||||
onClear={() => setSearchProjectValue("")}
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
"border-default-200 bg-transparent hover:bg-default-100/50",
|
||||
input: "text-small",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
topContent: shouldShowProjectSearch ? (
|
||||
<div className="sticky top-0 z-10 py-2">
|
||||
<Input
|
||||
isClearable
|
||||
placeholder="Search projects..."
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
startContent={<Search size={16} />}
|
||||
value={searchProjectValue}
|
||||
onValueChange={setSearchProjectValue}
|
||||
onClear={() => setSearchProjectValue("")}
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
"border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary",
|
||||
input: "text-small",
|
||||
clearButton: "text-default-400",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
}}
|
||||
>
|
||||
{filteredProjects.map(([key, name]) => (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DataTableRowActions } from "@/components/findings/table/data-table-row-
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import {
|
||||
DateWithTime,
|
||||
EntityInfoShort,
|
||||
EntityInfo,
|
||||
SnippetChip,
|
||||
} from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
@@ -91,8 +91,11 @@ const FindingDetailsCell = ({ row }: { row: any }) => {
|
||||
export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Details" />
|
||||
),
|
||||
cell: ({ row }) => <FindingDetailsCell row={row} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "check",
|
||||
@@ -115,9 +118,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{delta === "new" || delta === "changed" ? (
|
||||
<DeltaIndicator delta={delta} />
|
||||
) : (
|
||||
<div className="w-2" />
|
||||
)}
|
||||
) : null}
|
||||
<p className="mr-7 text-sm break-words whitespace-normal">
|
||||
{checktitle}
|
||||
</p>
|
||||
@@ -131,7 +132,9 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: "Resource name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Resource name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceName = getResourceData(row, "name");
|
||||
|
||||
@@ -143,6 +146,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "severity",
|
||||
@@ -210,7 +214,9 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
// },
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: "Region",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Region" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
@@ -220,18 +226,24 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "service",
|
||||
header: "Service",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Service" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { servicename } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-xs">{servicename}</p>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "cloudProvider",
|
||||
header: "Cloud Provider",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cloud Provider" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const provider = getProviderData(row, "provider");
|
||||
const alias = getProviderData(row, "alias");
|
||||
@@ -239,7 +251,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
|
||||
return (
|
||||
<>
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
@@ -247,12 +259,16 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
</>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@/components/shadcn";
|
||||
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { EntityInfoShort, InfoField } from "@/components/ui/entities";
|
||||
import { EntityInfo, InfoField } from "@/components/ui/entities";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import { SeverityBadge } from "@/components/ui/table/severity-badge";
|
||||
import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils";
|
||||
@@ -119,7 +119,7 @@ export const FindingDetail = ({
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={providerDetails.provider as ProviderType}
|
||||
entityAlias={providerDetails.alias}
|
||||
entityId={providerDetails.uid}
|
||||
@@ -222,7 +222,7 @@ export const FindingDetail = ({
|
||||
{/* CLI Command section */}
|
||||
{attributes.check_metadata.remediation.code.cli && (
|
||||
<InfoField label="CLI Command" variant="simple">
|
||||
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800">
|
||||
<Snippet>
|
||||
<span className="text-xs whitespace-pre-line">
|
||||
{attributes.check_metadata.remediation.code.cli}
|
||||
</span>
|
||||
|
||||
@@ -7,74 +7,81 @@ export const AzureProviderBadge: React.FC<IconSvgProps> = ({
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 256 256"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="none">
|
||||
<rect width="256" height="256" fill="#f4f2ed" rx="60" />
|
||||
<path
|
||||
fill="url(#skillIconsAzureLight0)"
|
||||
d="M94.674 34.002h59.182L92.42 216.032a9.44 9.44 0 0 1-8.94 6.419H37.422a9.42 9.42 0 0 1-9.318-8.026a9.4 9.4 0 0 1 .39-4.407L85.733 40.421A9.44 9.44 0 0 1 94.674 34z"
|
||||
/>
|
||||
<path
|
||||
fill="#0078d4"
|
||||
d="M180.674 156.095H86.826a4.34 4.34 0 0 0-4.045 2.75a4.34 4.34 0 0 0 1.079 4.771l60.305 56.287a9.48 9.48 0 0 0 6.468 2.548h53.141z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#skillIconsAzureLight1)"
|
||||
d="M94.675 34.002a9.36 9.36 0 0 0-8.962 6.544L28.565 209.863a9.412 9.412 0 0 0 8.882 12.588h47.247a10.1 10.1 0 0 0 7.75-6.592l11.397-33.586l40.708 37.968a9.63 9.63 0 0 0 6.059 2.21h52.943l-23.22-66.355l-67.689.016l41.428-122.11z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#skillIconsAzureLight2)"
|
||||
d="M170.264 40.412a9.42 9.42 0 0 0-8.928-6.41H95.379a9.42 9.42 0 0 1 8.928 6.41l57.241 169.604a9.43 9.43 0 0 1-1.273 8.509a9.43 9.43 0 0 1-7.655 3.928h65.959a9.43 9.43 0 0 0 7.654-3.929a9.42 9.42 0 0 0 1.272-8.508z"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="skillIconsAzureLight0"
|
||||
x1="116.244"
|
||||
x2="54.783"
|
||||
y1="47.967"
|
||||
y2="229.54"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#114a8b" />
|
||||
<stop offset="1" stopColor="#0669bc" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="skillIconsAzureLight1"
|
||||
x1="135.444"
|
||||
x2="121.227"
|
||||
y1="132.585"
|
||||
y2="137.392"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopOpacity="0.3" />
|
||||
<stop offset="0.071" stopOpacity="0.2" />
|
||||
<stop offset="0.321" stopOpacity="0.1" />
|
||||
<stop offset="0.623" stopOpacity="0.05" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="skillIconsAzureLight2"
|
||||
x1="127.625"
|
||||
x2="195.091"
|
||||
y1="42.671"
|
||||
y2="222.414"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3ccbf4" />
|
||||
<stop offset="1" stopColor="#2892df" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}) => {
|
||||
const uniqueId = React.useId();
|
||||
const gradientId0 = `azure-gradient-0-${uniqueId}`;
|
||||
const gradientId1 = `azure-gradient-1-${uniqueId}`;
|
||||
const gradientId2 = `azure-gradient-2-${uniqueId}`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 256 256"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="none">
|
||||
<rect width="256" height="256" fill="#f4f2ed" rx="60" />
|
||||
<path
|
||||
fill={`url(#${gradientId0})`}
|
||||
d="M94.674 34.002h59.182L92.42 216.032a9.44 9.44 0 0 1-8.94 6.419H37.422a9.42 9.42 0 0 1-9.318-8.026a9.4 9.4 0 0 1 .39-4.407L85.733 40.421A9.44 9.44 0 0 1 94.674 34z"
|
||||
/>
|
||||
<path
|
||||
fill="#0078d4"
|
||||
d="M180.674 156.095H86.826a4.34 4.34 0 0 0-4.045 2.75a4.34 4.34 0 0 0 1.079 4.771l60.305 56.287a9.48 9.48 0 0 0 6.468 2.548h53.141z"
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${gradientId1})`}
|
||||
d="M94.675 34.002a9.36 9.36 0 0 0-8.962 6.544L28.565 209.863a9.412 9.412 0 0 0 8.882 12.588h47.247a10.1 10.1 0 0 0 7.75-6.592l11.397-33.586l40.708 37.968a9.63 9.63 0 0 0 6.059 2.21h52.943l-23.22-66.355l-67.689.016l41.428-122.11z"
|
||||
/>
|
||||
<path
|
||||
fill={`url(#${gradientId2})`}
|
||||
d="M170.264 40.412a9.42 9.42 0 0 0-8.928-6.41H95.379a9.42 9.42 0 0 1 8.928 6.41l57.241 169.604a9.43 9.43 0 0 1-1.273 8.509a9.43 9.43 0 0 1-7.655 3.928h65.959a9.43 9.43 0 0 0 7.654-3.929a9.42 9.42 0 0 0 1.272-8.508z"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={gradientId0}
|
||||
x1="116.244"
|
||||
x2="54.783"
|
||||
y1="47.967"
|
||||
y2="229.54"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#114a8b" />
|
||||
<stop offset="1" stopColor="#0669bc" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={gradientId1}
|
||||
x1="135.444"
|
||||
x2="121.227"
|
||||
y1="132.585"
|
||||
y2="137.392"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopOpacity="0.3" />
|
||||
<stop offset="0.071" stopOpacity="0.2" />
|
||||
<stop offset="0.321" stopOpacity="0.1" />
|
||||
<stop offset="0.623" stopOpacity="0.05" />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={gradientId2}
|
||||
x1="127.625"
|
||||
x2="195.091"
|
||||
y1="42.671"
|
||||
y2="222.414"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#3ccbf4" />
|
||||
<stop offset="1" stopColor="#2892df" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,126 +1,144 @@
|
||||
import * as React from "react";
|
||||
import type { FC } from "react";
|
||||
import { useId } from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
import type { IconSvgProps } from "@/types";
|
||||
|
||||
export const M365ProviderBadge: React.FC<IconSvgProps> = ({
|
||||
export const M365ProviderBadge: FC<IconSvgProps> = ({
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height || 64}
|
||||
role="presentation"
|
||||
viewBox="0 0 256 256"
|
||||
width={size || width || 64}
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<rect width="256" height="256" rx="60" fill="#f4f2ed" />
|
||||
<g transform="scale(3.5) translate(2 2)">
|
||||
<g clipPath="url(#clip0)">
|
||||
<g clipPath="url(#clip1)">
|
||||
<path
|
||||
d="M53.1574 10.3146C52.1706 7.19669 49.2773 5.07764 46.007 5.07764L43.5352 5.07764C39.9228 5.07764 36.8237 7.65268 36.1621 11.2039L32.4891 30.9179L33.5912 27.2788C34.5491 24.1158 37.4644 21.9526 40.7692 21.9526H52.2499L58.8326 24.2562L62.3337 21.9644C59.0634 21.9644 56.1701 19.8336 55.1833 16.7157L53.1574 10.3146Z"
|
||||
fill="url(#paint0_radial)"
|
||||
/>
|
||||
<path
|
||||
d="M20.615 62.8082C21.5914 65.9426 24.4927 68.0777 27.7757 68.0777H32.6415C36.7421 68.0777 40.0824 64.7845 40.1408 60.6844L40.3984 42.5737L39.4114 45.86C38.459 49.0313 35.5396 51.2027 32.2284 51.2027H20.75L14.8141 48.4965L11.4807 51.2027C14.7636 51.2027 17.665 53.3378 18.6414 56.4722L20.615 62.8082Z"
|
||||
fill="url(#paint1_radial)"
|
||||
/>
|
||||
<path
|
||||
d="M45.5 5.07764H19.25C11.75 5.07764 7.25001 14.7496 4.25002 24.4216C0.695797 35.8804 -3.95498 51.2056 9.50001 51.2056H20.931C24.2656 51.2056 27.1975 49.0121 28.135 45.812C30.1073 39.0797 33.5545 27.3661 36.2631 18.446C37.6417 13.906 38.79 10.007 40.5523 7.57888C41.5404 6.21761 43.1871 5.07764 45.5 5.07764Z"
|
||||
fill="url(#paint2_linear)"
|
||||
/>
|
||||
<path
|
||||
d="M27.4946 68.0776H53.7446C61.2446 68.0776 65.7446 58.4071 68.7446 48.7365C72.2988 37.2794 76.9496 21.9565 63.4946 21.9565H52.0633C48.7288 21.9565 45.797 24.1499 44.8594 27.3499C42.8871 34.0812 39.44 45.7927 36.7314 54.7113C35.3529 59.2506 34.2046 63.149 32.4422 65.5768C31.4542 66.9378 29.8075 68.0776 27.4946 68.0776Z"
|
||||
fill="url(#paint4_radial)"
|
||||
/>
|
||||
<rect
|
||||
x="24.125"
|
||||
y="51.2031"
|
||||
width="48.375"
|
||||
height="21.375"
|
||||
rx="3.63727"
|
||||
fill="black"
|
||||
/>
|
||||
<text x="26" y="67" fill="white" fontSize="16" fontWeight="bold">
|
||||
M365
|
||||
</text>
|
||||
}) => {
|
||||
const uniqueId = useId();
|
||||
const gradientId0 = `m365-gradient-0-${uniqueId}`;
|
||||
const gradientId1 = `m365-gradient-1-${uniqueId}`;
|
||||
const gradientId2 = `m365-gradient-2-${uniqueId}`;
|
||||
const gradientId4 = `m365-gradient-4-${uniqueId}`;
|
||||
const clipId0 = `m365-clip-0-${uniqueId}`;
|
||||
const clipId1 = `m365-clip-1-${uniqueId}`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 256 256"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<rect width="256" height="256" rx="60" fill="#f4f2ed" />
|
||||
<g transform="scale(3.5) translate(2 2)">
|
||||
<g clipPath={`url(#${clipId0})`}>
|
||||
<g clipPath={`url(#${clipId1})`}>
|
||||
<path
|
||||
d="M53.1574 10.3146C52.1706 7.19669 49.2773 5.07764 46.007 5.07764L43.5352 5.07764C39.9228 5.07764 36.8237 7.65268 36.1621 11.2039L32.4891 30.9179L33.5912 27.2788C34.5491 24.1158 37.4644 21.9526 40.7692 21.9526H52.2499L58.8326 24.2562L62.3337 21.9644C59.0634 21.9644 56.1701 19.8336 55.1833 16.7157L53.1574 10.3146Z"
|
||||
fill={`url(#${gradientId0})`}
|
||||
/>
|
||||
<path
|
||||
d="M20.615 62.8082C21.5914 65.9426 24.4927 68.0777 27.7757 68.0777H32.6415C36.7421 68.0777 40.0824 64.7845 40.1408 60.6844L40.3984 42.5737L39.4114 45.86C38.459 49.0313 35.5396 51.2027 32.2284 51.2027H20.75L14.8141 48.4965L11.4807 51.2027C14.7636 51.2027 17.665 53.3378 18.6414 56.4722L20.615 62.8082Z"
|
||||
fill={`url(#${gradientId1})`}
|
||||
/>
|
||||
<path
|
||||
d="M45.5 5.07764H19.25C11.75 5.07764 7.25001 14.7496 4.25002 24.4216C0.695797 35.8804 -3.95498 51.2056 9.50001 51.2056H20.931C24.2656 51.2056 27.1975 49.0121 28.135 45.812C30.1073 39.0797 33.5545 27.3661 36.2631 18.446C37.6417 13.906 38.79 10.007 40.5523 7.57888C41.5404 6.21761 43.1871 5.07764 45.5 5.07764Z"
|
||||
fill={`url(#${gradientId2})`}
|
||||
/>
|
||||
<path
|
||||
d="M27.4946 68.0776H53.7446C61.2446 68.0776 65.7446 58.4071 68.7446 48.7365C72.2988 37.2794 76.9496 21.9565 63.4946 21.9565H52.0633C48.7288 21.9565 45.797 24.1499 44.8594 27.3499C42.8871 34.0812 39.44 45.7927 36.7314 54.7113C35.3529 59.2506 34.2046 63.149 32.4422 65.5768C31.4542 66.9378 29.8075 68.0776 27.4946 68.0776Z"
|
||||
fill={`url(#${gradientId4})`}
|
||||
/>
|
||||
<rect
|
||||
x="24.125"
|
||||
y="51.2031"
|
||||
width="48.375"
|
||||
height="21.375"
|
||||
rx="3.63727"
|
||||
fill="#131313"
|
||||
/>
|
||||
<text
|
||||
x="27.5"
|
||||
y="67"
|
||||
fill="#ffffff"
|
||||
fontFamily="Inter, Arial, sans-serif"
|
||||
fontSize="16"
|
||||
fontWeight="700"
|
||||
>
|
||||
M365
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={gradientId0}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(59.4363 31.0868) rotate(-130.285) scale(27.6431 26.1575)"
|
||||
>
|
||||
<stop offset="0.0955758" stopColor="#00AEFF" />
|
||||
<stop offset="0.773185" stopColor="#2253CE" />
|
||||
<stop offset="1" stopColor="#0736C4" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={gradientId1}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(15.3608 50.9716) rotate(50.2556) scale(25.0142 24.5538)"
|
||||
>
|
||||
<stop stopColor="#FFB657" />
|
||||
<stop offset="0.633728" stopColor="#FF5F3D" />
|
||||
<stop offset="0.923392" stopColor="#C02B3C" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id={gradientId2}
|
||||
x1="17.6789"
|
||||
y1="10.6669"
|
||||
x2="21.2461"
|
||||
y2="52.961"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.156162" stopColor="#0D91E1" />
|
||||
<stop offset="0.487484" stopColor="#52B471" />
|
||||
<stop offset="0.652394" stopColor="#98BD42" />
|
||||
<stop offset="0.937361" stopColor="#FFC800" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id={gradientId4}
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(64.843 17.441) rotate(109.722) scale(61.4524 75.0539)"
|
||||
>
|
||||
<stop offset="0.0661714" stopColor="#8C48FF" />
|
||||
<stop offset="0.5" stopColor="#F2598A" />
|
||||
<stop offset="0.895833" stopColor="#FFB152" />
|
||||
</radialGradient>
|
||||
<clipPath id={clipId0}>
|
||||
<rect
|
||||
width="72"
|
||||
height="72"
|
||||
fill="white"
|
||||
transform="translate(0.5 0.578125)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id={clipId1}>
|
||||
<rect
|
||||
width="72"
|
||||
height="72"
|
||||
fill="white"
|
||||
transform="translate(0.5 0.578125)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(59.4363 31.0868) rotate(-130.285) scale(27.6431 26.1575)"
|
||||
>
|
||||
<stop offset="0.0955758" stopColor="#00AEFF" />
|
||||
<stop offset="0.773185" stopColor="#2253CE" />
|
||||
<stop offset="1" stopColor="#0736C4" />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id="paint1_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(15.3608 50.9716) rotate(50.2556) scale(25.0142 24.5538)"
|
||||
>
|
||||
<stop stopColor="#FFB657" />
|
||||
<stop offset="0.633728" stopColor="#FF5F3D" />
|
||||
<stop offset="0.923392" stopColor="#C02B3C" />
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear"
|
||||
x1="17.6789"
|
||||
y1="10.6669"
|
||||
x2="21.2461"
|
||||
y2="52.961"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.156162" stopColor="#0D91E1" />
|
||||
<stop offset="0.487484" stopColor="#52B471" />
|
||||
<stop offset="0.652394" stopColor="#98BD42" />
|
||||
<stop offset="0.937361" stopColor="#FFC800" />
|
||||
</linearGradient>
|
||||
<radialGradient
|
||||
id="paint4_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(64.843 17.441) rotate(109.722) scale(61.4524 75.0539)"
|
||||
>
|
||||
<stop offset="0.0661714" stopColor="#8C48FF" />
|
||||
<stop offset="0.5" stopColor="#F2598A" />
|
||||
<stop offset="0.895833" stopColor="#FFB152" />
|
||||
</radialGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect
|
||||
width="72"
|
||||
height="72"
|
||||
fill="white"
|
||||
transform="translate(0.5 0.578125)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1">
|
||||
<rect
|
||||
width="72"
|
||||
height="72"
|
||||
fill="white"
|
||||
transform="translate(0.5 0.578125)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,11 +15,14 @@ const getInvitationData = (row: { original: InvitationProps }) => {
|
||||
export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: () => <div className="text-left">Email</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Email" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const data = getInvitationData(row);
|
||||
return <p className="font-semibold">{data?.email || "N/A"}</p>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "state",
|
||||
@@ -33,12 +36,15 @@ export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: () => <div className="text-left">Role</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Role" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const roleName =
|
||||
row.original.relationships?.role?.attributes?.name || "No Role";
|
||||
return <p className="font-semibold">{roleName}</p>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "inserted_at",
|
||||
@@ -71,11 +77,14 @@ export const ColumnsInvitation: ColumnDef<InvitationProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const roles = row.original.roles;
|
||||
return <DataTableRowActions row={row} roles={roles} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PaperclipIcon,
|
||||
PlusIcon,
|
||||
SendIcon,
|
||||
SquareIcon,
|
||||
StopCircleIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -935,7 +935,7 @@ export const PromptInputSubmit = ({
|
||||
if (status === "submitted") {
|
||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||
} else if (status === "streaming") {
|
||||
Icon = <SquareIcon className="size-4" />;
|
||||
Icon = <StopCircleIcon className="size-5" />;
|
||||
} else if (status === "error") {
|
||||
Icon = <XIcon className="size-4" />;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@ export const Chat = ({
|
||||
// Provider and model management
|
||||
const [providers, setProviders] = useState<Provider[]>(initialProviders);
|
||||
const loadedProvidersRef = useRef<Set<LighthouseProvider>>(new Set());
|
||||
const [loadingProviders, setLoadingProviders] = useState<
|
||||
Set<LighthouseProvider>
|
||||
>(new Set());
|
||||
|
||||
// Initialize selectedModel with defaults from props
|
||||
const [selectedModel, setSelectedModel] = useState<SelectedModel>(() => {
|
||||
@@ -142,6 +145,7 @@ export const Chat = ({
|
||||
|
||||
// Mark as loaded
|
||||
loadedProvidersRef.current.add(providerType);
|
||||
setLoadingProviders((prev) => new Set(prev).add(providerType));
|
||||
|
||||
try {
|
||||
const response = await getLighthouseModelIds(providerType);
|
||||
@@ -167,66 +171,79 @@ export const Chat = ({
|
||||
console.error(`Error loading models for ${providerType}:`, error);
|
||||
// Remove from loaded on error so it can be retried
|
||||
loadedProvidersRef.current.delete(providerType);
|
||||
} finally {
|
||||
setLoadingProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerType);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { messages, sendMessage, status, error, setMessages, regenerate } =
|
||||
useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/lighthouse/analyst",
|
||||
credentials: "same-origin",
|
||||
body: () => ({
|
||||
model: selectedModelRef.current.modelId,
|
||||
provider: selectedModelRef.current.providerType,
|
||||
}),
|
||||
const {
|
||||
messages,
|
||||
sendMessage,
|
||||
status,
|
||||
error,
|
||||
setMessages,
|
||||
regenerate,
|
||||
stop,
|
||||
} = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/lighthouse/analyst",
|
||||
credentials: "same-origin",
|
||||
body: () => ({
|
||||
model: selectedModelRef.current.modelId,
|
||||
provider: selectedModelRef.current.providerType,
|
||||
}),
|
||||
experimental_throttle: 100,
|
||||
onFinish: ({ message }) => {
|
||||
// There is no specific way to output the error message from langgraph supervisor
|
||||
// Hence, all error messages are sent as normal messages with the prefix [LIGHTHOUSE_ANALYST_ERROR]:
|
||||
// Detect error messages sent from backend using specific prefix and display the error
|
||||
const firstTextPart = message.parts.find((p) => p.type === "text");
|
||||
if (
|
||||
firstTextPart &&
|
||||
"text" in firstTextPart &&
|
||||
firstTextPart.text.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:")
|
||||
) {
|
||||
const errorText = firstTextPart.text
|
||||
.replace("[LIGHTHOUSE_ANALYST_ERROR]:", "")
|
||||
.trim();
|
||||
setErrorMessage(errorText);
|
||||
// Remove error message from chat history
|
||||
setMessages((prev) =>
|
||||
prev.filter((m) => {
|
||||
const textPart = m.parts.find((p) => p.type === "text");
|
||||
return !(
|
||||
textPart &&
|
||||
"text" in textPart &&
|
||||
textPart.text.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:")
|
||||
);
|
||||
}),
|
||||
);
|
||||
restoreLastUserMessage();
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Chat error:", error);
|
||||
|
||||
if (
|
||||
error?.message?.includes("<html>") &&
|
||||
error?.message?.includes("<title>403 Forbidden</title>")
|
||||
) {
|
||||
restoreLastUserMessage();
|
||||
setErrorMessage("403 Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
restoreLastUserMessage();
|
||||
setErrorMessage(
|
||||
error?.message || "An error occurred. Please retry your message.",
|
||||
}),
|
||||
experimental_throttle: 100,
|
||||
onFinish: ({ message }) => {
|
||||
// There is no specific way to output the error message from langgraph supervisor
|
||||
// Hence, all error messages are sent as normal messages with the prefix [LIGHTHOUSE_ANALYST_ERROR]:
|
||||
// Detect error messages sent from backend using specific prefix and display the error
|
||||
const firstTextPart = message.parts.find((p) => p.type === "text");
|
||||
if (
|
||||
firstTextPart &&
|
||||
"text" in firstTextPart &&
|
||||
firstTextPart.text.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:")
|
||||
) {
|
||||
const errorText = firstTextPart.text
|
||||
.replace("[LIGHTHOUSE_ANALYST_ERROR]:", "")
|
||||
.trim();
|
||||
setErrorMessage(errorText);
|
||||
// Remove error message from chat history
|
||||
setMessages((prev) =>
|
||||
prev.filter((m) => {
|
||||
const textPart = m.parts.find((p) => p.type === "text");
|
||||
return !(
|
||||
textPart &&
|
||||
"text" in textPart &&
|
||||
textPart.text.startsWith("[LIGHTHOUSE_ANALYST_ERROR]:")
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
restoreLastUserMessage();
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Chat error:", error);
|
||||
|
||||
if (
|
||||
error?.message?.includes("<html>") &&
|
||||
error?.message?.includes("<title>403 Forbidden</title>")
|
||||
) {
|
||||
restoreLastUserMessage();
|
||||
setErrorMessage("403 Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
restoreLastUserMessage();
|
||||
setErrorMessage(
|
||||
error?.message || "An error occurred. Please retry your message.",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const messagesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -264,6 +281,12 @@ export const Chat = ({
|
||||
}
|
||||
};
|
||||
|
||||
const stopGeneration = () => {
|
||||
if (status === "streaming" || status === "submitted") {
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive or when streaming
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current) {
|
||||
@@ -542,15 +565,18 @@ export const Chat = ({
|
||||
<Combobox
|
||||
value={`${selectedModel.providerType}:${selectedModel.modelId}`}
|
||||
onValueChange={(value) => {
|
||||
const [providerType, modelId] = value.split(":");
|
||||
const separatorIndex = value.indexOf(":");
|
||||
if (separatorIndex === -1) return;
|
||||
|
||||
const providerType = value.slice(
|
||||
0,
|
||||
separatorIndex,
|
||||
) as LighthouseProvider;
|
||||
const modelId = value.slice(separatorIndex + 1);
|
||||
const provider = providers.find((p) => p.id === providerType);
|
||||
const model = provider?.models.find((m) => m.id === modelId);
|
||||
if (provider && model) {
|
||||
handleModelSelect(
|
||||
providerType as LighthouseProvider,
|
||||
modelId,
|
||||
model.name,
|
||||
);
|
||||
handleModelSelect(providerType, modelId, model.name);
|
||||
}
|
||||
}}
|
||||
groups={providers.map((provider) => ({
|
||||
@@ -560,6 +586,8 @@ export const Chat = ({
|
||||
label: model.name,
|
||||
})),
|
||||
}))}
|
||||
loading={loadingProviders.size > 0}
|
||||
loadingMessage="Loading models..."
|
||||
placeholder={selectedModel.modelName || "Select model..."}
|
||||
searchPlaceholder="Search models..."
|
||||
emptyMessage="No model found."
|
||||
@@ -570,10 +598,21 @@ export const Chat = ({
|
||||
{/* Submit Button */}
|
||||
<PromptInputSubmit
|
||||
status={status}
|
||||
type={
|
||||
status === "streaming" || status === "submitted"
|
||||
? "button"
|
||||
: "submit"
|
||||
}
|
||||
onClick={(event) => {
|
||||
if (status === "streaming" || status === "submitted") {
|
||||
event.preventDefault();
|
||||
stopGeneration();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
status === "streaming" ||
|
||||
status === "submitted" ||
|
||||
!uiState.inputValue?.trim()
|
||||
!uiState.inputValue?.trim() &&
|
||||
status !== "streaming" &&
|
||||
status !== "submitted"
|
||||
}
|
||||
/>
|
||||
</PromptInputToolbar>
|
||||
|
||||
@@ -82,8 +82,10 @@ export const ColumnGroups: ColumnDef<ProviderGroup>[] = [
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,11 +10,15 @@ import { DeltaIndicator } from "@/components/findings/table/delta-indicator";
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import {
|
||||
DateWithTime,
|
||||
EntityInfoShort,
|
||||
EntityInfo,
|
||||
SnippetChip,
|
||||
} from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
|
||||
import {
|
||||
DataTableColumnHeader,
|
||||
SeverityBadge,
|
||||
StatusFindingBadge,
|
||||
} from "@/components/ui/table";
|
||||
import { FindingProps, ProviderType } from "@/types";
|
||||
|
||||
const getFindingsData = (row: { original: FindingProps }) => {
|
||||
@@ -72,12 +76,17 @@ const FindingDetailsCell = ({ row }: { row: any }) => {
|
||||
export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Details" />
|
||||
),
|
||||
cell: ({ row }) => <FindingDetailsCell row={row} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "check",
|
||||
header: "Finding",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Finding" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
const {
|
||||
@@ -102,10 +111,13 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: "Resource name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Resource name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceName = getResourceData(row, "name");
|
||||
|
||||
@@ -117,20 +129,26 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "severity",
|
||||
header: "Severity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Severity" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { severity },
|
||||
} = getFindingsData(row);
|
||||
return <SeverityBadge severity={severity} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { status },
|
||||
@@ -138,10 +156,13 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
|
||||
return <StatusFindingBadge size="sm" status={status} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "updated_at",
|
||||
header: "Last seen",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Last seen" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { updated_at },
|
||||
@@ -152,10 +173,13 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: "Region",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Region" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
@@ -165,18 +189,24 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "service",
|
||||
header: "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: "Cloud Provider",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cloud Provider" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const provider = getProviderData(row, "provider");
|
||||
const alias = getProviderData(row, "alias");
|
||||
@@ -184,7 +214,7 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
|
||||
return (
|
||||
<>
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
@@ -192,5 +222,6 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
</>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -45,21 +45,27 @@ export const ColumnProviders: ColumnDef<ProviderProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "scanJobs",
|
||||
header: "Scan Jobs",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Scan Jobs" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { uid },
|
||||
} = getProviderData(row);
|
||||
return <LinkToScans providerUid={uid} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "groupNames",
|
||||
header: "Groups",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Groups" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { groupNames } = getProviderData(row);
|
||||
return <GroupNameChips groupNames={groupNames || []} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "uid",
|
||||
@@ -95,9 +101,11 @@ export const ColumnProviders: ColumnDef<ProviderProps>[] = [
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Database } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { EntityInfoShort, SnippetChip } from "@/components/ui/entities";
|
||||
import { EntityInfo, SnippetChip } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { ProviderType, ResourceProps } from "@/types";
|
||||
@@ -62,12 +62,17 @@ const ResourceDetailsCell = ({ row }: { row: any }) => {
|
||||
export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Details" />
|
||||
),
|
||||
cell: ({ row }) => <ResourceDetailsCell row={row} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: "Resource name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Resource name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceName = getResourceData(row, "name");
|
||||
const displayName =
|
||||
@@ -83,10 +88,13 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "failedFindings",
|
||||
header: () => <div className="text-center">Failed Findings</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Failed Findings" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const failedFindingsCount = getResourceData(
|
||||
row,
|
||||
@@ -94,17 +102,14 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
) as number;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-center">
|
||||
<span
|
||||
className={`mx-auto flex h-6 w-6 items-center justify-center rounded-full bg-yellow-100 text-xs font-semibold text-yellow-800 ${getChipStyle(failedFindingsCount)}`}
|
||||
>
|
||||
{failedFindingsCount}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
<span
|
||||
className={`ml-10 flex h-6 w-6 items-center justify-center rounded-full bg-yellow-100 text-xs font-semibold text-yellow-800 ${getChipStyle(failedFindingsCount)}`}
|
||||
>
|
||||
{failedFindingsCount}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
@@ -157,14 +162,16 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "provider",
|
||||
header: "Cloud Provider",
|
||||
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 (
|
||||
<>
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias && typeof alias === "string" ? alias : undefined}
|
||||
entityId={uid && typeof uid === "string" ? uid : undefined}
|
||||
@@ -172,5 +179,6 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
</>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -105,10 +105,13 @@ export const ColumnsRoles: ColumnDef<RolesProps["data"][number]>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/shadcn";
|
||||
import { EntityInfoShort } from "@/components/ui/entities";
|
||||
import { EntityInfo } from "@/components/ui/entities";
|
||||
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
|
||||
|
||||
interface SelectScanProviderProps<
|
||||
@@ -54,7 +54,7 @@ export const SelectScanProvider = <
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a cloud provider">
|
||||
{selectedItem ? (
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={
|
||||
selectedItem.providerType as
|
||||
| "aws"
|
||||
@@ -64,7 +64,7 @@ export const SelectScanProvider = <
|
||||
}
|
||||
entityAlias={selectedItem.alias}
|
||||
entityId={selectedItem.uid}
|
||||
hideCopyButton
|
||||
showCopyAction={false}
|
||||
/>
|
||||
) : (
|
||||
"Choose a cloud provider"
|
||||
@@ -74,7 +74,7 @@ export const SelectScanProvider = <
|
||||
<SelectContent>
|
||||
{providers.map((item) => (
|
||||
<SelectItem key={item.providerId} value={item.providerId}>
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={
|
||||
item.providerType as
|
||||
| "aws"
|
||||
@@ -84,7 +84,7 @@ export const SelectScanProvider = <
|
||||
}
|
||||
entityAlias={item.alias}
|
||||
entityId={item.uid}
|
||||
hideCopyButton
|
||||
showCopyAction={false}
|
||||
/>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import { Snippet } from "@heroui/snippet";
|
||||
|
||||
import {
|
||||
DateWithTime,
|
||||
EntityInfoShort,
|
||||
InfoField,
|
||||
} from "@/components/ui/entities";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities";
|
||||
import { StatusBadge } from "@/components/ui/table/status-badge";
|
||||
import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types";
|
||||
|
||||
@@ -28,21 +25,6 @@ const formatDuration = (seconds: number) => {
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
const Section = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="dark:bg-prowler-blue-400 flex flex-col gap-4 rounded-lg p-4 shadow">
|
||||
<h3 className="text-md dark:text-prowler-theme-pale/90 font-medium text-gray-800">
|
||||
{title}
|
||||
</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ScanDetail = ({
|
||||
scanDetails,
|
||||
}: {
|
||||
@@ -68,7 +50,7 @@ export const ScanDetail = ({
|
||||
loadingProgress={scan.progress}
|
||||
/>
|
||||
</div>
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={providerDetails?.provider as ProviderType}
|
||||
entityAlias={providerDetails?.alias}
|
||||
entityId={providerDetails?.uid}
|
||||
@@ -77,63 +59,63 @@ export const ScanDetail = ({
|
||||
</div>
|
||||
|
||||
{/* Scan Details */}
|
||||
<Section title="Scan Details">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Scan Name">{renderValue(scan.name)}</InfoField>
|
||||
<InfoField label="Resources Scanned">
|
||||
{scan.unique_resource_count}
|
||||
</InfoField>
|
||||
<InfoField label="Progress">{scan.progress}%</InfoField>
|
||||
</div>
|
||||
<Card variant="base" padding="lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Scan Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Scan Name">{renderValue(scan.name)}</InfoField>
|
||||
<InfoField label="Resources Scanned">
|
||||
{scan.unique_resource_count}
|
||||
</InfoField>
|
||||
<InfoField label="Progress">{scan.progress}%</InfoField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Trigger">{renderValue(scan.trigger)}</InfoField>
|
||||
<InfoField label="State">{renderValue(scan.state)}</InfoField>
|
||||
<InfoField label="Duration">
|
||||
{formatDuration(scan.duration)}
|
||||
</InfoField>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Trigger">{renderValue(scan.trigger)}</InfoField>
|
||||
<InfoField label="State">{renderValue(scan.state)}</InfoField>
|
||||
<InfoField label="Duration">
|
||||
{formatDuration(scan.duration)}
|
||||
</InfoField>
|
||||
</div>
|
||||
|
||||
<InfoField label="Scan ID" variant="simple">
|
||||
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol>
|
||||
{scanDetails.id}
|
||||
</Snippet>
|
||||
</InfoField>
|
||||
<InfoField label="Scan ID" variant="simple">
|
||||
<Snippet hideSymbol>{scanDetails.id}</Snippet>
|
||||
</InfoField>
|
||||
|
||||
{scan.state === "failed" && taskDetails?.attributes.result && (
|
||||
<>
|
||||
{taskDetails.attributes.result.exc_message && (
|
||||
<InfoField label="Error Message" variant="simple">
|
||||
<Snippet
|
||||
className="bg-gray-50 py-1 dark:bg-slate-800"
|
||||
hideSymbol
|
||||
>
|
||||
<span className="text-xs whitespace-pre-line">
|
||||
{taskDetails.attributes.result.exc_message.join("\n")}
|
||||
</span>
|
||||
</Snippet>
|
||||
</InfoField>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InfoField label="Error Type">
|
||||
{renderValue(taskDetails.attributes.result.exc_type)}
|
||||
</InfoField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{scan.state === "failed" && taskDetails?.attributes.result && (
|
||||
<>
|
||||
{taskDetails.attributes.result.exc_message && (
|
||||
<InfoField label="Error Message" variant="simple">
|
||||
<Snippet hideSymbol>
|
||||
<span className="text-xs whitespace-pre-line">
|
||||
{taskDetails.attributes.result.exc_message.join("\n")}
|
||||
</span>
|
||||
</Snippet>
|
||||
</InfoField>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InfoField label="Error Type">
|
||||
{renderValue(taskDetails.attributes.result.exc_type)}
|
||||
</InfoField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Started At">
|
||||
<DateWithTime inline dateTime={scan.started_at || "-"} />
|
||||
</InfoField>
|
||||
<InfoField label="Completed At">
|
||||
<DateWithTime inline dateTime={scan.completed_at || "-"} />
|
||||
</InfoField>
|
||||
<InfoField label="Scheduled At">
|
||||
<DateWithTime inline dateTime={scan.scheduled_at || "-"} />
|
||||
</InfoField>
|
||||
</div>
|
||||
</Section>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<InfoField label="Started At">
|
||||
<DateWithTime inline dateTime={scan.started_at || "-"} />
|
||||
</InfoField>
|
||||
<InfoField label="Completed At">
|
||||
<DateWithTime inline dateTime={scan.completed_at || "-"} />
|
||||
</InfoField>
|
||||
<InfoField label="Scheduled At">
|
||||
<DateWithTime inline dateTime={scan.scheduled_at || "-"} />
|
||||
</InfoField>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { TableLink } from "@/components/ui/custom";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { DateWithTime, EntityInfo } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
|
||||
import { ProviderType, ScanProps } from "@/types";
|
||||
@@ -67,12 +66,17 @@ const ScanDetailsCell = ({ row }: { row: any }) => {
|
||||
export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
{
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Details" />
|
||||
),
|
||||
cell: ({ row }) => <ScanDetailsCell row={row} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "cloudProvider",
|
||||
header: () => <p className="pr-8">Cloud Provider</p>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cloud Provider" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const providerInfo = row.original.providerInfo;
|
||||
|
||||
@@ -83,18 +87,21 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
const { provider, uid, alias } = providerInfo;
|
||||
|
||||
return (
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={provider as ProviderType}
|
||||
entityAlias={alias}
|
||||
entityId={uid}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "started_at",
|
||||
header: () => <p className="pr-8">Started at</p>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Started at" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { started_at },
|
||||
@@ -106,10 +113,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { state },
|
||||
@@ -123,10 +133,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "findings",
|
||||
header: "Findings",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Findings" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { id } = getScanData(row);
|
||||
const scanState = row.original.attributes?.state;
|
||||
@@ -138,10 +151,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "compliance",
|
||||
header: "Compliance",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Compliance" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { id } = getScanData(row);
|
||||
const scanState = row.original.attributes?.state;
|
||||
@@ -153,21 +169,12 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
id: "download",
|
||||
header: () => (
|
||||
<div className="flex items-end gap-x-1">
|
||||
<p className="w-fit text-xs">Download</p>
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content="Download a ZIP file that includes the JSON (OCSF), CSV, and HTML scan reports, along with the compliance report."
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<InfoIcon className="text-primary mb-1" size={12} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Download" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
@@ -176,21 +183,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// {
|
||||
// accessorKey: "scanner_args",
|
||||
// header: "Scanner Args",
|
||||
// cell: ({ row }) => {
|
||||
// const {
|
||||
// attributes: { scanner_args },
|
||||
// } = getScanData(row);
|
||||
// return <p className="font-medium">{scanner_args?.only_logs}</p>;
|
||||
// },
|
||||
// },
|
||||
{
|
||||
accessorKey: "resources",
|
||||
header: "Resources",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Resources" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { unique_resource_count },
|
||||
@@ -201,16 +200,20 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "scheduled_at",
|
||||
header: "Scheduled at",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Scheduled at" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { scheduled_at },
|
||||
} = getScanData(row);
|
||||
return <DateWithTime dateTime={scheduled_at} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "completed_at",
|
||||
@@ -272,8 +275,10 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,58 +1,64 @@
|
||||
import { Card, CardContent, CardHeader } from "@/components/shadcn";
|
||||
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
|
||||
|
||||
export const SkeletonScanDetail = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 rounded-lg">
|
||||
{/* Header Skeleton */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-default-200 h-8 w-24 animate-pulse rounded-full" />
|
||||
<Skeleton className="h-8 w-24 rounded-full" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-default-200 relative h-8 w-8 animate-pulse rounded-full" />
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="bg-default-200 h-4 w-32 animate-pulse rounded" />
|
||||
<div className="bg-default-200 h-3 w-24 animate-pulse rounded" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scan Details Section Skeleton */}
|
||||
<div className="dark:bg-prowler-blue-400 flex flex-col gap-4 rounded-lg p-4 shadow">
|
||||
<div className="bg-default-200 h-5 w-32 animate-pulse rounded" />
|
||||
<Card variant="base" padding="lg">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{/* First grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid1-${index}`} className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* First grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid1-${index}`} className="flex flex-col gap-2">
|
||||
<div className="bg-default-200 h-4 w-24 animate-pulse rounded" />
|
||||
<div className="bg-default-200 h-5 w-full animate-pulse rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Second grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid2-${index}`} className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Second grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid2-${index}`} className="flex flex-col gap-2">
|
||||
<div className="bg-default-200 h-4 w-20 animate-pulse rounded" />
|
||||
<div className="bg-default-200 h-5 w-full animate-pulse rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Scan ID field */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
{/* Scan ID field */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="bg-default-200 h-4 w-20 animate-pulse rounded" />
|
||||
<div className="bg-default-200 h-10 w-full animate-pulse rounded" />
|
||||
</div>
|
||||
|
||||
{/* Third grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid3-${index}`} className="flex flex-col gap-2">
|
||||
<div className="bg-default-200 h-4 w-24 animate-pulse rounded" />
|
||||
<div className="bg-default-200 h-5 w-full animate-pulse rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Third grid row */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={`grid3-${index}`} className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ export const ResourceStatsCard = ({
|
||||
>
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<div className="flex h-[51px] w-full flex-col items-start justify-center md:items-center">
|
||||
<p className="text-text-neutral-secondary text-center text-sm leading-5 font-medium">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
@@ -72,6 +72,8 @@ export interface ComboboxProps
|
||||
contentClassName?: string;
|
||||
disabled?: boolean;
|
||||
showSelectedFirst?: boolean;
|
||||
loading?: boolean;
|
||||
loadingMessage?: string;
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
@@ -88,6 +90,8 @@ export function Combobox({
|
||||
variant = "default",
|
||||
disabled = false,
|
||||
showSelectedFirst = true,
|
||||
loading = false,
|
||||
loadingMessage = "Loading...",
|
||||
}: ComboboxProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -127,8 +131,16 @@ export function Combobox({
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder={searchPlaceholder} className="h-9" />
|
||||
{!loading && (
|
||||
<CommandInput placeholder={searchPlaceholder} className="h-9" />
|
||||
)}
|
||||
<CommandList className="minimal-scrollbar max-h-[400px]">
|
||||
{loading && (
|
||||
<div className="text-text-neutral-tertiary flex items-center gap-2 px-3 py-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>{loadingMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||
|
||||
{/* Show selected option first if enabled */}
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./separator/separator";
|
||||
export * from "./skeleton/skeleton";
|
||||
export * from "./tabs/generic-tabs";
|
||||
export * from "./tabs/tabs";
|
||||
export * from "./tooltip";
|
||||
|
||||
@@ -131,13 +131,15 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-bg-button-secondary text-bg-button-secondary relative flex w-full cursor-pointer items-center gap-2 rounded-lg px-4 py-3 text-sm outline-hidden select-none hover:bg-slate-200 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 dark:hover:bg-slate-700/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-bg-button-secondary text-bg-button-secondary relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-3 pr-12 pl-4 text-sm outline-hidden select-none hover:bg-slate-200 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 dark:hover:bg-slate-700/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText asChild>
|
||||
<span className="flex min-w-0 items-center gap-2">{children}</span>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{children}
|
||||
</span>
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator asChild>
|
||||
<CheckIcon className="text-bg-button-secondary absolute right-4 size-5" />
|
||||
|
||||
@@ -120,7 +120,10 @@ export const Accordion = ({
|
||||
|
||||
return (
|
||||
<NextUIAccordion
|
||||
className={cn("w-full px-0!", className)}
|
||||
className={cn(
|
||||
"bg-bg-neutral-primary border-border-neutral-secondary w-full rounded-lg border",
|
||||
className,
|
||||
)}
|
||||
variant={variant}
|
||||
selectionMode={selectionMode}
|
||||
selectedKeys={expandedKeys}
|
||||
@@ -139,7 +142,7 @@ export const Accordion = ({
|
||||
isDisabled={item.isDisabled}
|
||||
indicator={<ChevronDown className="text-gray-500" />}
|
||||
classNames={{
|
||||
base: index === 0 || index === 1 ? "my-1" : "my-1",
|
||||
base: index === 0 || index === 1 ? "my-2" : "my-2",
|
||||
title: "text-sm",
|
||||
subtitle: "text-xs text-gray-500",
|
||||
trigger:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
import React from "react";
|
||||
|
||||
import { IdIcon } from "@/components/icons";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { getProviderLogo } from "./get-provider-logo";
|
||||
import { SnippetChip } from "./snippet-chip";
|
||||
|
||||
interface EntityInfoProps {
|
||||
cloudProvider: ProviderType;
|
||||
entityAlias?: string;
|
||||
entityId?: string;
|
||||
hideCopyButton?: boolean;
|
||||
snippetWidth?: string;
|
||||
showConnectionStatus?: boolean;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
export const EntityInfoShort: React.FC<EntityInfoProps> = ({
|
||||
cloudProvider,
|
||||
entityAlias,
|
||||
entityId,
|
||||
hideCopyButton = false,
|
||||
showConnectionStatus = false,
|
||||
maxWidth = "max-w-[120px]",
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-start">
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<div className="relative shrink-0">
|
||||
{getProviderLogo(cloudProvider)}
|
||||
{showConnectionStatus && (
|
||||
<Tooltip
|
||||
size="sm"
|
||||
content={showConnectionStatus ? "Connected" : "Not Connected"}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-[-0.1rem] right-[-0.2rem] h-2 w-2 cursor-pointer rounded-full ${
|
||||
showConnectionStatus ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex ${maxWidth} flex-col gap-1`}>
|
||||
{entityAlias && (
|
||||
<Tooltip content={entityAlias} placement="top" size="sm">
|
||||
<span className="text-default-500 truncate text-xs text-ellipsis">
|
||||
{entityAlias}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SnippetChip
|
||||
value={entityId ?? ""}
|
||||
hideCopyButton={hideCopyButton}
|
||||
icon={<IdIcon className="size-4" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { CopyIcon, DoneIcon } from "@/components/icons";
|
||||
import type { ProviderType } from "@/types";
|
||||
|
||||
import { getProviderLogo } from "./get-provider-logo";
|
||||
|
||||
interface EntityInfoProps {
|
||||
cloudProvider: ProviderType;
|
||||
entityAlias?: string;
|
||||
entityId?: string;
|
||||
snippetWidth?: string;
|
||||
showConnectionStatus?: boolean;
|
||||
maxWidth?: string;
|
||||
showCopyAction?: boolean;
|
||||
}
|
||||
|
||||
export const EntityInfo = ({
|
||||
cloudProvider,
|
||||
entityAlias,
|
||||
entityId,
|
||||
showConnectionStatus = false,
|
||||
maxWidth = "w-[120px]",
|
||||
showCopyAction = true,
|
||||
}: EntityInfoProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copied) return undefined;
|
||||
|
||||
const timer = setTimeout(() => setCopied(false), 1400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
const handleCopyEntityId = async () => {
|
||||
if (!entityId) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(entityId);
|
||||
setCopied(true);
|
||||
} catch (_error) {
|
||||
setCopied(false);
|
||||
}
|
||||
};
|
||||
|
||||
const canCopy = Boolean(entityId && showCopyAction);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative shrink-0">
|
||||
{getProviderLogo(cloudProvider)}
|
||||
{showConnectionStatus && (
|
||||
<Tooltip
|
||||
size="sm"
|
||||
content={showConnectionStatus ? "Connected" : "Not Connected"}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-[-0.1rem] right-[-0.2rem] h-2 w-2 cursor-pointer rounded-full ${
|
||||
showConnectionStatus ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex ${maxWidth} flex-col gap-1`}>
|
||||
{entityAlias ? (
|
||||
<Tooltip content={entityAlias} placement="top-start" size="sm">
|
||||
<p className="text-text-neutral-primary truncate text-left text-xs font-medium">
|
||||
{entityAlias}
|
||||
</p>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip content="No alias" placement="top-start" size="sm">
|
||||
<p className="text-text-neutral-secondary truncate text-left text-xs">
|
||||
-
|
||||
</p>
|
||||
</Tooltip>
|
||||
)}
|
||||
{entityId && (
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<Tooltip content={entityId} placement="top-start" size="sm">
|
||||
<p className="text-text-neutral-secondary min-w-0 truncate text-left text-xs">
|
||||
{entityId}
|
||||
</p>
|
||||
</Tooltip>
|
||||
{canCopy && (
|
||||
<Tooltip
|
||||
content={copied ? "Copied" : "Copy to clipboard"}
|
||||
placement="top"
|
||||
size="sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyEntityId}
|
||||
aria-label="Copiar ID de la entidad"
|
||||
className="hover:bg-bg-neutral-tertiary focus-visible:ring-bg-data-info text-text-neutral-secondary hover:text-text-neutral-primary rounded-md p-1 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
{copied ? <DoneIcon size={14} /> : <CopyIcon size={14} />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./date-with-time";
|
||||
export * from "./entity-info-short";
|
||||
export * from "./entity-info";
|
||||
export * from "./get-provider-logo";
|
||||
export * from "./info-field";
|
||||
export * from "./scan-status";
|
||||
|
||||
@@ -75,14 +75,18 @@ export const DataTableColumnHeader = <TData, TValue>({
|
||||
};
|
||||
|
||||
if (!column.getCanSort()) {
|
||||
return <div>{title}</div>;
|
||||
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">
|
||||
<span className="block break-normal whitespace-nowrap">{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex h-10 w-full items-center justify-between px-0 text-left align-middle text-sm font-semibold whitespace-nowrap text-slate-500 dark:text-slate-400"
|
||||
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"
|
||||
onClick={getToggleSortingHandler}
|
||||
>
|
||||
<span className="block break-normal whitespace-nowrap">{title}</span>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
MultiSelectTrigger,
|
||||
MultiSelectValue,
|
||||
} from "@/components/shadcn/select/multiselect";
|
||||
import { EntityInfoShort } from "@/components/ui/entities/entity-info-short";
|
||||
import { EntityInfo } from "@/components/ui/entities/entity-info";
|
||||
import { useUrlFilters } from "@/hooks/use-url-filters";
|
||||
import { isConnectionStatus, isScanEntity } from "@/lib/helper-filters";
|
||||
import {
|
||||
@@ -78,11 +78,11 @@ export const DataTableFilterCustom = ({
|
||||
// Provider entity
|
||||
const providerEntity = entity as ProviderEntity;
|
||||
return (
|
||||
<EntityInfoShort
|
||||
<EntityInfo
|
||||
cloudProvider={providerEntity.provider}
|
||||
entityAlias={providerEntity.alias ?? undefined}
|
||||
entityId={providerEntity.uid}
|
||||
hideCopyButton
|
||||
showCopyAction={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,10 +21,7 @@ const TableHeader = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"[&>tr]:first:shadow-small [&>tr]:first:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
className={cn("[&>tr]:first:rounded-lg", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -35,7 +35,9 @@ export const createApiKeyColumns = (
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
header: "Email",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Email" />
|
||||
),
|
||||
cell: ({ row }) => <EmailCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -52,7 +54,9 @@ export const createApiKeyColumns = (
|
||||
},
|
||||
{
|
||||
accessorKey: "last_used_at",
|
||||
header: "Last Used",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Last Used" />
|
||||
),
|
||||
cell: ({ row }) => <LastUsedCell apiKey={row.original} />,
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -76,11 +80,12 @@ export const createApiKeyColumns = (
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<DataTableRowActions row={row} onEdit={onEdit} onRevoke={onRevoke} />
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -35,11 +35,14 @@ export const ColumnsUser: ColumnDef<UserProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: () => <div className="text-left">Role</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Role" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { role } = getUserData(row);
|
||||
return <p className="font-semibold">{role?.name || "No Role"}</p>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "company_name",
|
||||
@@ -72,11 +75,14 @@ export const ColumnsUser: ColumnDef<UserProps>[] = [
|
||||
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Actions" />
|
||||
),
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const roles = row.original.roles;
|
||||
return <DataTableRowActions row={row} roles={roles} />;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user