Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/pages/audit-report/components/View/View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
TextPanel,
DurationPanel,
BarGaugePanel,
PropertiesPanel
PropertiesPanel,
PlaybooksPanel
} from "./panels";
import GlobalFilters from "./GlobalFilters";
import GlobalFiltersForm from "./GlobalFiltersForm";
Expand Down Expand Up @@ -232,7 +233,7 @@ const View: React.FC<ViewProps> = ({
<div className="flex-none">
{title !== "" && (
<h3 className="mb-4 flex items-center text-xl font-semibold">
<Box className="mr-2 text-teal-600" size={20} />
<Box className="text-teal-600 mr-2" size={20} />
{title}
</h3>
)}
Expand Down Expand Up @@ -427,6 +428,8 @@ const renderPanel = (panel: PanelResult, index: number) => {
return <DurationPanel key={`${panel.name}-${index}`} summary={panel} />;
case "properties":
return <PropertiesPanel key={`${panel.name}-${index}`} summary={panel} />;
case "playbooks":
return <PlaybooksPanel key={`${panel.name}-${index}`} summary={panel} />;
default:
return null;
}
Expand Down
10 changes: 3 additions & 7 deletions src/pages/audit-report/components/View/panels/BarGaugePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { PanelResult, BarGaugeConfig } from "../../../types";
import { getGaugeColor, formatDisplayValue } from "./utils";
import PanelWrapper from "./PanelWrapper";

interface BarGaugePanelProps {
summary: PanelResult;
Expand Down Expand Up @@ -60,12 +61,7 @@ const BarGaugePanel: React.FC<BarGaugePanelProps> = ({ summary }) => {
};

return (
<div className="flex h-full w-full flex-col rounded-lg border border-gray-200 bg-white p-4">
<h4 className="mb-2 text-sm font-medium text-gray-600">{summary.name}</h4>
{summary.description && (
<p className="mb-3 text-xs text-gray-500">{summary.description}</p>
)}

<PanelWrapper title={summary.name} description={summary.description}>
<div className="flex flex-col gap-3">
{summary.rows.map((row, rowIndex) => {
const labelValue =
Expand Down Expand Up @@ -127,7 +123,7 @@ const BarGaugePanel: React.FC<BarGaugePanelProps> = ({ summary }) => {
);
})}
</div>
</div>
</PanelWrapper>
);
};

Expand Down
17 changes: 6 additions & 11 deletions src/pages/audit-report/components/View/panels/DurationPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { formatDuration } from "@flanksource-ui/utils/date";
import { PanelResult } from "../../../types";
import PanelWrapper from "./PanelWrapper";

interface DurationPanelProps {
summary: PanelResult;
Expand All @@ -22,24 +23,18 @@ const DurationPanel: React.FC<DurationPanelProps> = ({ summary }) => {
const formattedDuration = formatDuration(milliseconds);

return (
<div
<PanelWrapper
key={`${summary.name}-${rowIndex}`}
className="flex h-full w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4"
title={label || summary.name}
description={summary.description}
titleClassName="capitalize"
>
<h4 className="mb-2 text-sm font-medium capitalize text-gray-600">
{label || summary.name}
</h4>
{summary.description && (
<p className="mb-3 text-xs text-gray-500">
{summary.description}
</p>
)}
<div className="flex flex-1 items-center justify-center">
<p className="text-teal-600 text-2xl font-semibold md:text-3xl lg:text-4xl">
{formattedDuration}
</p>
</div>
</div>
</PanelWrapper>
);
})}
</>
Expand Down
17 changes: 6 additions & 11 deletions src/pages/audit-report/components/View/panels/NumberPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { PanelResult } from "../../../types";
import { formatDisplayValue } from "./utils";
import PanelWrapper from "./PanelWrapper";

interface NumberPanelProps {
summary: PanelResult;
Expand All @@ -19,18 +20,12 @@ const NumberPanel: React.FC<NumberPanelProps> = ({ summary }) => {
const numericValue = Number(displayValue);

return (
<div
<PanelWrapper
key={`${summary.name}-${rowIndex}`}
className="flex h-full w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4"
title={label || summary.name}
description={summary.description}
titleClassName="capitalize"
>
<h4 className="mb-2 text-sm font-medium capitalize text-gray-600">
{label || summary.name}
</h4>
{summary.description && (
<p className="mb-3 text-xs text-gray-500">
{summary.description}
</p>
)}
<div className="flex flex-1 items-center justify-center">
<p className="text-6xl font-bold sm:text-6xl md:text-6xl lg:text-6xl">
{summary.number
Expand All @@ -42,7 +37,7 @@ const NumberPanel: React.FC<NumberPanelProps> = ({ summary }) => {
: displayValue}
</p>
</div>
</div>
</PanelWrapper>
);
})}
</>
Expand Down
39 changes: 39 additions & 0 deletions src/pages/audit-report/components/View/panels/PanelWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { ReactNode } from "react";

interface PanelWrapperProps {
title: string;
description?: string;
className?: string;
titleClassName?: string;
children: ReactNode;
}

/**
* Common wrapper component for all panel types.
* Provides consistent styling for the panel container, header, and description.
*/
const PanelWrapper: React.FC<PanelWrapperProps> = ({
title,
description,
className = "",
titleClassName = "",
children
}) => {
return (
<div
className={`flex h-full w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 ${className}`}
>
<h4
className={`mb-2 text-sm font-medium text-gray-600 ${titleClassName}`}
>
{title}
</h4>
{description && (
<p className="mb-3 text-xs text-gray-500">{description}</p>
)}
{children}
</div>
);
};

export default PanelWrapper;
13 changes: 7 additions & 6 deletions src/pages/audit-report/components/View/panels/PieChartPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "recharts";
import { PanelResult } from "../../../types";
import { COLOR_PALETTE, getSeverityOfText, severityToHex } from "./utils";
import PanelWrapper from "./PanelWrapper";

interface PieChartPanelProps {
summary: PanelResult;
Expand Down Expand Up @@ -98,11 +99,11 @@ const PieChartPanel: React.FC<PieChartPanelProps> = ({ summary }) => {
);

return (
<div className="flex h-full min-h-[300px] w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4">
<h4 className="mb-2 text-sm font-medium text-gray-600">{summary.name}</h4>
{summary.description && (
<p className="mb-3 text-xs text-gray-500">{summary.description}</p>
)}
<PanelWrapper
title={summary.name}
description={summary.description}
className="min-h-[300px]"
>
<div className="flex flex-1 items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
Expand All @@ -126,7 +127,7 @@ const PieChartPanel: React.FC<PieChartPanelProps> = ({ summary }) => {
</PieChart>
</ResponsiveContainer>
</div>
</div>
</PanelWrapper>
);
};

Expand Down
179 changes: 179 additions & 0 deletions src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { useMemo, useState, useRef, useEffect } from "react";
import { Loader2, ChevronDown, Workflow } from "lucide-react";
import { useGetPlaybookSpecsDetails } from "@flanksource-ui/api/query-hooks/playbooks";
import { SubmitPlaybookRunFormValues } from "@flanksource-ui/components/Playbooks/Runs/Submit/SubmitPlaybookRunForm";
import SubmitPlaybookRunForm from "@flanksource-ui/components/Playbooks/Runs/Submit/SubmitPlaybookRunForm";
import { Icon } from "@flanksource-ui/ui/Icons/Icon";
import EmptyState from "@flanksource-ui/components/EmptyState";
import { PanelResult } from "../../../types";
import mixins from "@flanksource-ui/utils/mixins.module.css";
import { Button } from "@flanksource-ui/components/ui/button";
import PanelWrapper from "./PanelWrapper";

type PlaybookRunRow = {
id: string;
title?: string;
name?: string;
description?: string;
icon?: string;
component_id?: string;
config_id?: string;
check_id?: string;
params?: SubmitPlaybookRunFormValues["params"];
};

type PlaybookRunPanelProps = {
summary: PanelResult;
};

/**
* A panel that lists playbooks with inline Run buttons and opens the existing
* SubmitPlaybookRunForm modal when a playbook is selected.
*/
export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) {
const rows = useMemo(
() =>
(summary.rows || []).filter(
(row): row is PlaybookRunRow =>
typeof row === "object" &&
row !== null &&
typeof row.id === "string" &&
row.id.length > 0
),
[summary.rows]
);
const [selected, setSelected] = useState<PlaybookRunRow | null>(null);
const [showScrollIndicator, setShowScrollIndicator] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);

const { data: playbookSpec, isLoading } = useGetPlaybookSpecsDetails(
selected?.id ?? "",
{
enabled: !!selected?.id
}
);

const handleCloseModal = () => {
setSelected(null);
};

// Check scroll position to show/hide indicator
const checkScrollPosition = () => {
const container = scrollContainerRef.current;
if (!container) return;

const { scrollTop, scrollHeight, clientHeight } = container;
const isScrollable = scrollHeight > clientHeight;
const isAtBottom = scrollHeight - scrollTop - clientHeight < 10; // 10px threshold

setShowScrollIndicator(isScrollable && !isAtBottom);
};

// Set up scroll listener and check initial state
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;

checkScrollPosition();

container.addEventListener("scroll", checkScrollPosition);
return () => container.removeEventListener("scroll", checkScrollPosition);
}, [rows]); // Re-check when rows change

return (
<PanelWrapper title={summary.name} description={summary.description}>
<div className="flex flex-1 flex-col overflow-hidden">
{/* Playbook List */}
<div className="relative min-h-0 flex-1">
<div
ref={scrollContainerRef}
className={`h-full overflow-y-auto ${mixins.hoverScrollbar}`}
>
{(!rows || rows.length === 0) && (
<EmptyState title="No playbooks" />
)}

{rows && rows.length > 0 && (
<div className="space-y-1">
{rows.map((row) => {
const title = row.title || row.name || "Playbook";
const isCurrentlyLoading =
isLoading && selected?.id === row.id;

return (
<div
key={row.id}
className="group rounded-lg border border-transparent px-3 py-2 transition-all hover:border-gray-200 hover:bg-gray-50"
>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex-shrink-0">
{row.icon ? (
<Icon
name={row.icon}
className="h-4 w-4 text-gray-600"
/>
) : (
<Workflow className="h-4 w-4 text-gray-600" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground">
{title}
</p>
{row.description && (
<p className="mt-0.5 text-xs text-muted-foreground">
{row.description}
</p>
)}
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setSelected(row)}
disabled={isCurrentlyLoading}
className="h-7 flex-shrink-0 gap-1.5 px-4"
>
{isCurrentlyLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-xs">Loading</span>
</>
) : (
<span className="text-xs font-medium">Run</span>
)}
</Button>
</div>
</div>
);
})}
</div>
)}
</div>

{/* Scroll Indicator */}
{showScrollIndicator && (
<div className="pointer-events-none absolute bottom-0 left-0 right-0 flex justify-center pb-2">
<div className="flex flex-col items-center gap-1 rounded-full bg-gradient-to-t from-white via-white to-transparent px-3 py-2">
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</div>
</div>
)}
</div>
</div>

{playbookSpec && selected && playbookSpec.id === selected.id && (
<SubmitPlaybookRunForm
isOpen={!!selected}
onClose={handleCloseModal}
playbook={playbookSpec}
componentId={selected.component_id}
configId={selected.config_id}
checkId={selected.check_id}
params={selected.params}
/>
)}
</PanelWrapper>
);
}
Loading
Loading