mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(ui): add error tooltip and status icon support to tree-view
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
import { Checkbox } from "@/components/shadcn/checkbox";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TreeLeafProps } from "@/types/tree";
|
||||
|
||||
@@ -30,6 +35,25 @@ export function TreeLeaf({
|
||||
renderItem,
|
||||
}: TreeLeafProps) {
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
const shouldReplaceCheckboxWithState =
|
||||
showCheckboxes && (item.isLoading || Boolean(item.status));
|
||||
const statusIcon =
|
||||
!item.isLoading && item.status ? (
|
||||
item.status === "error" && item.errorMessage ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<TreeStatusIcon status={item.status} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">{item.errorMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<TreeStatusIcon status={item.status} />
|
||||
)
|
||||
) : null;
|
||||
|
||||
const handleSelect = () => {
|
||||
if (!item.disabled) {
|
||||
@@ -50,7 +74,6 @@ export function TreeLeaf({
|
||||
"flex items-center gap-2 rounded-md px-2 py-1.5",
|
||||
"hover:bg-prowler-white/5 cursor-pointer",
|
||||
"focus-visible:ring-border-input-primary-press focus-visible:ring-2 focus-visible:outline-none",
|
||||
isSelected && "bg-prowler-white/10",
|
||||
item.disabled && "cursor-not-allowed opacity-50",
|
||||
item.className,
|
||||
)}
|
||||
@@ -62,12 +85,17 @@ export function TreeLeaf({
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={item.disabled}
|
||||
>
|
||||
{item.isLoading && <TreeSpinner />}
|
||||
{!item.isLoading && item.status && (
|
||||
<TreeStatusIcon status={item.status} />
|
||||
{!showCheckboxes && item.isLoading && <TreeSpinner />}
|
||||
{!showCheckboxes && statusIcon}
|
||||
|
||||
{showCheckboxes && shouldReplaceCheckboxWithState && (
|
||||
<>
|
||||
{item.isLoading && <TreeSpinner />}
|
||||
{statusIcon}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showCheckboxes && (
|
||||
{showCheckboxes && !shouldReplaceCheckboxWithState && (
|
||||
<Checkbox
|
||||
size="sm"
|
||||
checked={isSelected}
|
||||
|
||||
@@ -5,6 +5,11 @@ import { ChevronRightIcon } from "lucide-react";
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
import { Checkbox } from "@/components/shadcn/checkbox";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TreeNodeProps } from "@/types/tree";
|
||||
|
||||
@@ -34,11 +39,27 @@ export function TreeNode({
|
||||
onExpandedChange,
|
||||
showCheckboxes,
|
||||
renderItem,
|
||||
expandAll,
|
||||
enableSelectChildren,
|
||||
}: TreeNodeProps) {
|
||||
const isExpanded = expandAll || expandedIds.includes(item.id);
|
||||
const isExpanded = expandedIds.includes(item.id);
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
const statusIcon =
|
||||
!item.isLoading && item.status ? (
|
||||
item.status === "error" && item.errorMessage ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<TreeStatusIcon status={item.status} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">{item.errorMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<TreeStatusIcon status={item.status} />
|
||||
)
|
||||
) : null;
|
||||
|
||||
// Calculate indeterminate state based on descendant selection
|
||||
const descendantIds = getAllDescendantIds(item);
|
||||
@@ -91,7 +112,6 @@ export function TreeNode({
|
||||
"flex items-center gap-2 rounded-md px-2 py-1.5",
|
||||
"hover:bg-prowler-white/5 cursor-pointer",
|
||||
"focus-visible:ring-border-input-primary-press focus-visible:ring-2 focus-visible:outline-none",
|
||||
isSelected && "bg-prowler-white/10",
|
||||
item.disabled && "cursor-not-allowed opacity-50",
|
||||
item.className,
|
||||
)}
|
||||
@@ -125,9 +145,7 @@ export function TreeNode({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!item.isLoading && item.status && (
|
||||
<TreeStatusIcon status={item.status} />
|
||||
)}
|
||||
{statusIcon}
|
||||
|
||||
{showCheckboxes && (
|
||||
<Checkbox
|
||||
@@ -170,7 +188,7 @@ export function TreeNode({
|
||||
>
|
||||
{item.children?.map((child) => (
|
||||
<li key={child.id}>
|
||||
{child.children ? (
|
||||
{child.children && child.children.length > 0 ? (
|
||||
<TreeNode
|
||||
item={child}
|
||||
level={level + 1}
|
||||
@@ -180,7 +198,6 @@ export function TreeNode({
|
||||
onExpandedChange={onExpandedChange}
|
||||
showCheckboxes={showCheckboxes}
|
||||
renderItem={renderItem}
|
||||
expandAll={expandAll}
|
||||
enableSelectChildren={enableSelectChildren}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -9,6 +9,25 @@ import { TreeLeaf } from "./tree-leaf";
|
||||
import { TreeNode } from "./tree-node";
|
||||
import { getAllDescendantIds } from "./utils";
|
||||
|
||||
function getInitialExpandedIds(data: TreeDataItem[] | TreeDataItem): string[] {
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
|
||||
const expandableIds: string[] = [];
|
||||
const stack = [...items];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current) continue;
|
||||
|
||||
if (current.children && current.children.length > 0) {
|
||||
expandableIds.push(current.id);
|
||||
stack.push(...current.children);
|
||||
}
|
||||
}
|
||||
|
||||
return expandableIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* TreeView component for rendering hierarchical data structures.
|
||||
*
|
||||
@@ -56,7 +75,9 @@ export function TreeView({
|
||||
renderItem,
|
||||
}: TreeViewProps) {
|
||||
const [internalSelectedIds, setInternalSelectedIds] = useState<string[]>([]);
|
||||
const [internalExpandedIds, setInternalExpandedIds] = useState<string[]>([]);
|
||||
const [internalExpandedIds, setInternalExpandedIds] = useState<string[]>(
|
||||
expandAll ? getInitialExpandedIds(data) : [],
|
||||
);
|
||||
|
||||
const selectedIds = controlledSelectedIds ?? internalSelectedIds;
|
||||
const expandedIds = controlledExpandedIds ?? internalExpandedIds;
|
||||
@@ -108,7 +129,7 @@ export function TreeView({
|
||||
<ul className="space-y-1">
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
{item.children ? (
|
||||
{item.children && item.children.length > 0 ? (
|
||||
<TreeNode
|
||||
item={item}
|
||||
level={0}
|
||||
@@ -118,7 +139,6 @@ export function TreeView({
|
||||
onExpandedChange={handleExpandedChange}
|
||||
showCheckboxes={showCheckboxes}
|
||||
renderItem={renderItem}
|
||||
expandAll={expandAll}
|
||||
enableSelectChildren={enableSelectChildren}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TreeDataItem } from "@/types/tree";
|
||||
* Used to calculate consistent padding for nested tree items.
|
||||
*/
|
||||
export const TREE_INDENT_REM = 1.25;
|
||||
export const TREE_LEAF_EXTRA_PADDING_REM = 1.5;
|
||||
export const TREE_LEAF_EXTRA_PADDING_REM = 1.75;
|
||||
|
||||
/**
|
||||
* Calculates the left padding for a tree node based on its nesting level.
|
||||
|
||||
+2
-1
@@ -35,6 +35,8 @@ export interface TreeDataItem {
|
||||
isLoading?: boolean;
|
||||
/** Status indicator shown after loading (success/error) */
|
||||
status?: TreeItemStatus;
|
||||
/** Optional error detail used by status icon tooltip */
|
||||
errorMessage?: string;
|
||||
/** Additional CSS classes for the item */
|
||||
className?: string;
|
||||
}
|
||||
@@ -97,7 +99,6 @@ export interface TreeNodeProps {
|
||||
onExpandedChange: (ids: string[]) => void;
|
||||
showCheckboxes: boolean;
|
||||
renderItem?: (params: TreeRenderItemParams) => React.ReactNode;
|
||||
expandAll: boolean;
|
||||
enableSelectChildren: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user