mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
style(ui): improve shadcn primitives and add shared components (#10153)
This commit is contained in:
@@ -5,7 +5,7 @@ import { Spacer } from "@heroui/spacer";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { VerticalSteps } from "@/components/providers/workflow/vertical-steps";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { LighthouseProvider } from "@/types/lighthouse";
|
||||
|
||||
import { getProviderConfig } from "../llm-provider-registry";
|
||||
@@ -75,12 +75,60 @@ export const WorkflowConnectLLM = () => {
|
||||
value={currentStep + 1}
|
||||
valueLabel={`${currentStep + 1} of ${steps.length}`}
|
||||
/>
|
||||
<VerticalSteps
|
||||
hideProgressBars
|
||||
currentStep={currentStep}
|
||||
stepClassName="border border-border-neutral-primary aria-[current]:bg-bg-neutral-primary cursor-default"
|
||||
steps={steps}
|
||||
/>
|
||||
<nav aria-label="Progress">
|
||||
<ol className="flex flex-col gap-y-3">
|
||||
{steps.map((step, index) => {
|
||||
const isActive = index === currentStep;
|
||||
const isComplete = index < currentStep;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={step.title}
|
||||
className="border-border-neutral-primary rounded-large border px-3 py-2.5"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[34px] w-[34px] items-center justify-center rounded-full border text-sm font-semibold",
|
||||
isComplete &&
|
||||
"bg-button-primary border-button-primary text-white",
|
||||
isActive &&
|
||||
"border-button-primary text-button-primary bg-transparent",
|
||||
!isActive &&
|
||||
!isComplete &&
|
||||
"text-default-500 border-border-neutral-primary bg-transparent",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div
|
||||
className={cn(
|
||||
"text-medium font-medium",
|
||||
isActive || isComplete
|
||||
? "text-default-foreground"
|
||||
: "text-default-500",
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-small",
|
||||
isActive || isComplete
|
||||
? "text-default-600"
|
||||
: "text-default-500",
|
||||
)}
|
||||
>
|
||||
{step.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
<Spacer y={4} />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from "./forms/delete-form";
|
||||
export * from "./link-to-scans";
|
||||
export * from "./muted-findings-config-button";
|
||||
export * from "./provider-info";
|
||||
export * from "./radio-card";
|
||||
export * from "./radio-group-provider";
|
||||
|
||||
66
ui/components/providers/radio-card.tsx
Normal file
66
ui/components/providers/radio-card.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RadioCardProps {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
/** Optional trailing content (e.g. a CTA badge). */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function RadioCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
onClick,
|
||||
selected = false,
|
||||
disabled = false,
|
||||
children,
|
||||
}: RadioCardProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex min-h-[72px] w-full items-center gap-4 rounded-lg border px-3 py-2.5 text-left transition-colors",
|
||||
disabled
|
||||
? "border-border-neutral-primary bg-bg-neutral-tertiary cursor-not-allowed"
|
||||
: selected
|
||||
? "border-primary bg-bg-neutral-tertiary cursor-pointer"
|
||||
: "hover:border-primary border-border-neutral-primary bg-bg-neutral-tertiary cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"size-[18px] shrink-0 rounded-full border shadow-xs",
|
||||
selected
|
||||
? "border-primary bg-primary"
|
||||
: "border-border-neutral-primary bg-bg-input-primary",
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<Icon
|
||||
className={cn(
|
||||
"size-[18px] shrink-0",
|
||||
disabled ? "text-text-neutral-tertiary" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-sm leading-6",
|
||||
disabled ? "text-text-neutral-tertiary" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const buttonVariants = cva(
|
||||
"border border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary active:bg-border-neutral-tertiary text-text-neutral-primary focus-visible:ring-border-neutral-tertiary/50",
|
||||
ghost:
|
||||
"text-text-neutral-primary hover:bg-bg-neutral-tertiary active:bg-border-neutral-secondary focus-visible:ring-border-neutral-secondary/50",
|
||||
link: "text-button-tertiary underline-offset-4 hover:text-button-tertiary-hover",
|
||||
link: "text-button-tertiary underline-offset-4 hover:text-button-tertiary-hover disabled:bg-transparent",
|
||||
// Menu variant like secondary but more padding and the back is almost transparent
|
||||
menu: "backdrop-blur-xl bg-white/60 dark:bg-white/5 border border-white/80 dark:border-white/10 text-text-neutral-primary dark:text-white shadow-lg hover:bg-white/70 dark:hover:bg-white/10 hover:border-white/90 dark:hover:border-white/30 active:bg-white/80 dark:active:bg-white/15 active:scale-[0.98] focus-visible:ring-button-primary/50 transition-all duration-200",
|
||||
"menu-active":
|
||||
@@ -33,6 +33,7 @@ const buttonVariants = cva(
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
xl: "h-12 rounded-md px-8 text-base has-[>svg]:px-6",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
|
||||
@@ -46,9 +46,9 @@ function Checkbox({
|
||||
// Default state
|
||||
"bg-bg-input-primary border-border-input-primary shadow-[0_1px_2px_0_rgba(0,0,0,0.1)]",
|
||||
// Checked state
|
||||
"data-[state=checked]:bg-button-primary data-[state=checked]:border-button-primary data-[state=checked]:text-white",
|
||||
"data-[state=checked]:bg-button-tertiary-active data-[state=checked]:border-button-tertiary-active data-[state=checked]:text-white",
|
||||
// Indeterminate state
|
||||
"data-[state=indeterminate]:bg-button-primary data-[state=indeterminate]:border-button-primary data-[state=indeterminate]:text-white",
|
||||
"data-[state=indeterminate]:bg-button-tertiary-active data-[state=indeterminate]:border-button-tertiary-active data-[state=indeterminate]:text-white",
|
||||
// Focus state
|
||||
"focus-visible:border-border-input-primary-press focus-visible:ring-border-input-primary-press/50 focus-visible:ring-2",
|
||||
// Disabled state
|
||||
|
||||
@@ -11,7 +11,7 @@ const inputVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1 placeholder:text-text-neutral-tertiary",
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-inset focus:ring-border-input-primary-press placeholder:text-text-neutral-tertiary",
|
||||
ghost:
|
||||
"border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary placeholder:text-text-neutral-tertiary",
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ const searchInputVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-border-input-primary-press focus:ring-offset-1",
|
||||
"border-border-input-primary bg-bg-input-primary dark:bg-input/30 hover:bg-bg-neutral-secondary dark:hover:bg-input/50 focus:border-border-input-primary-press focus:ring-1 focus:ring-inset focus:ring-border-input-primary-press",
|
||||
ghost:
|
||||
"border-transparent bg-transparent hover:bg-bg-neutral-tertiary focus:bg-bg-neutral-tertiary",
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"border-border-neutral-tertiary bg-bg-neutral-tertiary text-text-neutral-primary animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-lg border px-3 py-1.5 text-xs text-balance shadow-lg",
|
||||
"border-border-neutral-tertiary bg-bg-neutral-tertiary text-text-neutral-primary animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit max-w-[min(700px,calc(100vw-2rem))] origin-(--radix-tooltip-content-transform-origin) rounded-lg border px-2 py-1.5 text-left text-xs break-all whitespace-normal shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -3,6 +3,7 @@ export { TreeLeaf } from "./tree-leaf";
|
||||
export { TreeNode } from "./tree-node";
|
||||
export { TreeSpinner } from "./tree-spinner";
|
||||
export { TreeStatusIcon } from "./tree-status-icon";
|
||||
export { TreeStatusIndicator } from "./tree-status-indicator";
|
||||
export { TreeView } from "./tree-view";
|
||||
export {
|
||||
getAllDescendantIds,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TreeLeafProps } from "@/types/tree";
|
||||
|
||||
import { TreeItemLabel } from "./tree-item-label";
|
||||
import { TreeSpinner } from "./tree-spinner";
|
||||
import { TreeStatusIcon } from "./tree-status-icon";
|
||||
import { TreeStatusIndicator } from "./tree-status-indicator";
|
||||
import { getTreeLeafPadding } from "./utils";
|
||||
|
||||
/**
|
||||
@@ -30,6 +30,15 @@ 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 ? (
|
||||
<TreeStatusIndicator
|
||||
status={item.status}
|
||||
errorMessage={item.errorMessage}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const handleSelect = () => {
|
||||
if (!item.disabled) {
|
||||
@@ -50,7 +59,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 +70,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}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TreeNodeProps } from "@/types/tree";
|
||||
import { TreeItemLabel } from "./tree-item-label";
|
||||
import { TreeLeaf } from "./tree-leaf";
|
||||
import { TreeSpinner } from "./tree-spinner";
|
||||
import { TreeStatusIcon } from "./tree-status-icon";
|
||||
import { TreeStatusIndicator } from "./tree-status-indicator";
|
||||
import { getAllDescendantIds, getTreeNodePadding } from "./utils";
|
||||
|
||||
/**
|
||||
@@ -34,11 +34,17 @@ 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 ? (
|
||||
<TreeStatusIndicator
|
||||
status={item.status}
|
||||
errorMessage={item.errorMessage}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
// Calculate indeterminate state based on descendant selection
|
||||
const descendantIds = getAllDescendantIds(item);
|
||||
@@ -91,7 +97,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 +130,7 @@ export function TreeNode({
|
||||
)}
|
||||
</button>
|
||||
|
||||
{!item.isLoading && item.status && (
|
||||
<TreeStatusIcon status={item.status} />
|
||||
)}
|
||||
{statusIcon}
|
||||
|
||||
{showCheckboxes && (
|
||||
<Checkbox
|
||||
@@ -170,7 +173,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 +183,6 @@ export function TreeNode({
|
||||
onExpandedChange={onExpandedChange}
|
||||
showCheckboxes={showCheckboxes}
|
||||
renderItem={renderItem}
|
||||
expandAll={expandAll}
|
||||
enableSelectChildren={enableSelectChildren}
|
||||
/>
|
||||
) : (
|
||||
|
||||
41
ui/components/shadcn/tree-view/tree-status-indicator.tsx
Normal file
41
ui/components/shadcn/tree-view/tree-status-indicator.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { TreeItemStatus } from "@/types/tree";
|
||||
|
||||
import { TreeStatusIcon } from "./tree-status-icon";
|
||||
|
||||
interface TreeStatusIndicatorProps {
|
||||
status?: TreeItemStatus;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export function TreeStatusIndicator({
|
||||
status,
|
||||
errorMessage,
|
||||
}: TreeStatusIndicatorProps) {
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === "error" && errorMessage) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<TreeStatusIcon status={status} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">{errorMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <TreeStatusIcon status={status} />;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from "./use-credentials-form";
|
||||
export * from "./use-form-server-errors";
|
||||
export * from "./use-local-storage";
|
||||
export * from "./use-related-filters";
|
||||
export * from "./use-scroll-hint";
|
||||
export * from "./use-sidebar";
|
||||
export * from "./use-store";
|
||||
export * from "./use-url-filters";
|
||||
|
||||
63
ui/hooks/use-scroll-hint.ts
Normal file
63
ui/hooks/use-scroll-hint.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { UIEvent, useEffect, useRef, useState } from "react";
|
||||
|
||||
interface UseScrollHintOptions {
|
||||
enabled?: boolean;
|
||||
refreshToken?: string | number;
|
||||
}
|
||||
|
||||
const SCROLL_THRESHOLD_PX = 4;
|
||||
|
||||
function shouldShowScrollHint(element: HTMLDivElement) {
|
||||
const hasOverflow =
|
||||
element.scrollHeight - element.clientHeight > SCROLL_THRESHOLD_PX;
|
||||
const isAtBottom =
|
||||
element.scrollTop + element.clientHeight >=
|
||||
element.scrollHeight - SCROLL_THRESHOLD_PX;
|
||||
|
||||
return hasOverflow && !isAtBottom;
|
||||
}
|
||||
|
||||
export function useScrollHint({
|
||||
enabled = true,
|
||||
refreshToken,
|
||||
}: UseScrollHintOptions = {}) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showScrollHint, setShowScrollHint] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setShowScrollHint(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const element = containerRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const recalculate = () => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
setShowScrollHint(shouldShowScrollHint(el));
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(recalculate);
|
||||
observer.observe(element);
|
||||
|
||||
recalculate();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [enabled, refreshToken]);
|
||||
|
||||
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||
setShowScrollHint(shouldShowScrollHint(event.currentTarget));
|
||||
};
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
showScrollHint,
|
||||
handleScroll,
|
||||
};
|
||||
}
|
||||
@@ -80,55 +80,55 @@ export const addProviderFormSchema = z
|
||||
z.object({
|
||||
providerType: z.literal("aws"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(12, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("azure"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("m365"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("gcp"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("kubernetes"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
awsCredentialsType: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("github"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("iac"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("oraclecloud"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("mongodbatlas"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("alibabacloud"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Provider ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("cloudflare"),
|
||||
|
||||
@@ -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