feat(api, web): implement policy mode management for agents and rules
- Added support for policy modes ('blacklist' and 'whitelist') in agent and policy set management.
- Updated API endpoints to handle policy mode during agent assignment and rule operations.
- Enhanced the web UI to display and manage policy modes for agents and rules, ensuring all assigned sets share a consistent mode.
- Introduced new validation to enforce single policy mode across assigned sets for agents.
- Improved error handling for policy mode conflicts and updated documentation accordingly.
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
createContext,
|
||||
CSSProperties,
|
||||
isValidElement,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react"
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import {
|
||||
defaultDropAnimationSideEffects,
|
||||
DndContext,
|
||||
DragCancelEvent,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
DropAnimation,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
Modifiers,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
UniqueIdentifier,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DraggableSyntheticListeners,
|
||||
} from "@dnd-kit/core"
|
||||
import {
|
||||
arrayMove,
|
||||
defaultAnimateLayoutChanges,
|
||||
rectSortingStrategy,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
type AnimateLayoutChanges,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { createPortal } from "react-dom"
|
||||
|
||||
import { cn } from "@evofw/ui/lib/utils"
|
||||
|
||||
// Sortable Item Context
|
||||
const SortableItemContext = createContext<{
|
||||
listeners: DraggableSyntheticListeners | undefined
|
||||
isDragging?: boolean
|
||||
disabled?: boolean
|
||||
}>({
|
||||
listeners: undefined,
|
||||
isDragging: false,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const IsOverlayContext = createContext(false)
|
||||
|
||||
const SortableInternalContext = createContext<{
|
||||
activeId: UniqueIdentifier | null
|
||||
modifiers?: Modifiers
|
||||
}>({
|
||||
activeId: null,
|
||||
modifiers: undefined,
|
||||
})
|
||||
|
||||
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
|
||||
defaultAnimateLayoutChanges({ ...args, wasDragging: true })
|
||||
|
||||
const dropAnimationConfig: DropAnimation = {
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: {
|
||||
opacity: "0.4",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const MOUSE_SENSOR_OPTIONS = { activationConstraint: { distance: 10 } }
|
||||
const TOUCH_SENSOR_OPTIONS = {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
}
|
||||
const KEYBOARD_SENSOR_OPTIONS = {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}
|
||||
const MEASURING_CONFIG = {
|
||||
droppable: { strategy: MeasuringStrategy.Always },
|
||||
}
|
||||
const STRATEGY_MAP = {
|
||||
horizontal: rectSortingStrategy,
|
||||
grid: rectSortingStrategy,
|
||||
vertical: verticalListSortingStrategy,
|
||||
} as const
|
||||
|
||||
// Multipurpose Sortable Component
|
||||
export interface SortableCommitMeta<T> {
|
||||
event: DragEndEvent
|
||||
activeIndex: number
|
||||
overIndex: number
|
||||
previousValue: T[]
|
||||
}
|
||||
|
||||
export interface SortableRootProps<T> extends Omit<
|
||||
useRender.ComponentProps<"div">,
|
||||
"onDragStart" | "onDragEnd" | "children"
|
||||
> {
|
||||
value: T[]
|
||||
onValueChange: (value: T[]) => void
|
||||
getItemValue: (item: T) => string
|
||||
children: ReactNode
|
||||
onMove?: (event: {
|
||||
event: DragEndEvent
|
||||
activeIndex: number
|
||||
overIndex: number
|
||||
}) => void
|
||||
onValueCommit?: (value: T[], meta: SortableCommitMeta<T>) => void
|
||||
strategy?: "horizontal" | "vertical" | "grid"
|
||||
onDragStart?: (event: DragStartEvent) => void
|
||||
onDragEnd?: (event: DragEndEvent) => void
|
||||
onDragCancel?: (event: DragCancelEvent) => void
|
||||
accessibility?: React.ComponentProps<typeof DndContext>["accessibility"]
|
||||
modifiers?: Modifiers
|
||||
}
|
||||
|
||||
function Sortable<T>({
|
||||
value,
|
||||
onValueChange,
|
||||
getItemValue,
|
||||
className,
|
||||
render,
|
||||
onMove,
|
||||
onValueCommit,
|
||||
strategy = "vertical",
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onDragCancel,
|
||||
accessibility,
|
||||
modifiers,
|
||||
children,
|
||||
...props
|
||||
}: SortableRootProps<T>) {
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useLayoutEffect(() => setMounted(true), [])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, MOUSE_SENSOR_OPTIONS),
|
||||
useSensor(TouchSensor, TOUCH_SENSOR_OPTIONS),
|
||||
useSensor(KeyboardSensor, KEYBOARD_SENSOR_OPTIONS)
|
||||
)
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragStartEvent) => {
|
||||
setActiveId(event.active.id)
|
||||
onDragStart?.(event)
|
||||
},
|
||||
[onDragStart]
|
||||
)
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
onDragEnd?.(event)
|
||||
|
||||
if (!over) return
|
||||
|
||||
// Handle item reordering
|
||||
const activeIndex = value.findIndex(
|
||||
(item: T) => getItemValue(item) === active.id
|
||||
)
|
||||
const overIndex = value.findIndex(
|
||||
(item: T) => getItemValue(item) === over.id
|
||||
)
|
||||
|
||||
if (activeIndex === -1 || overIndex === -1) return
|
||||
|
||||
if (activeIndex !== overIndex) {
|
||||
if (onMove) {
|
||||
onMove({ event, activeIndex, overIndex })
|
||||
} else {
|
||||
const newValue = arrayMove(value, activeIndex, overIndex)
|
||||
onValueChange(newValue)
|
||||
onValueCommit?.(newValue, {
|
||||
event,
|
||||
activeIndex,
|
||||
overIndex,
|
||||
previousValue: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[value, getItemValue, onValueChange, onMove, onDragEnd, onValueCommit]
|
||||
)
|
||||
|
||||
const handleDragCancel = useCallback(
|
||||
(event: DragCancelEvent) => {
|
||||
setActiveId(null)
|
||||
onDragCancel?.(event)
|
||||
},
|
||||
[onDragCancel]
|
||||
)
|
||||
|
||||
const itemIds = useMemo(() => {
|
||||
const ids = value.map(getItemValue)
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const seen = new Set<string>()
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) {
|
||||
console.warn(
|
||||
`[Sortable] Duplicate item id "${id}". Item ids must be unique, or drag and drop will misbehave.`
|
||||
)
|
||||
break
|
||||
}
|
||||
seen.add(id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}, [value, getItemValue])
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ activeId, modifiers }),
|
||||
[activeId, modifiers]
|
||||
)
|
||||
|
||||
const defaultProps = {
|
||||
"data-slot": "sortable",
|
||||
"data-dragging": activeId !== null,
|
||||
className: cn(activeId !== null && "cursor-grabbing!", className),
|
||||
children,
|
||||
}
|
||||
|
||||
// Find the active child for the overlay
|
||||
const overlayContent = useMemo(() => {
|
||||
if (!activeId) return null
|
||||
let result: ReactNode = null
|
||||
Children.forEach(children, (child) => {
|
||||
if (isValidElement(child) && (child.props as any).value === activeId) {
|
||||
result = cloneElement(child as ReactElement<any>, {
|
||||
...(child.props as any),
|
||||
className: cn((child.props as any).className, "z-50"),
|
||||
})
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, [activeId, children])
|
||||
|
||||
return (
|
||||
<SortableInternalContext.Provider value={contextValue}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={modifiers}
|
||||
accessibility={accessibility}
|
||||
measuring={MEASURING_CONFIG}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext
|
||||
items={itemIds}
|
||||
strategy={STRATEGY_MAP[strategy] ?? verticalListSortingStrategy}
|
||||
>
|
||||
{useRender({
|
||||
defaultTagName: "div",
|
||||
render,
|
||||
props: mergeProps<"div">(defaultProps, props),
|
||||
})}
|
||||
</SortableContext>
|
||||
{mounted &&
|
||||
createPortal(
|
||||
<DragOverlay
|
||||
dropAnimation={dropAnimationConfig}
|
||||
modifiers={modifiers}
|
||||
className={cn("z-50", activeId && "cursor-grabbing")}
|
||||
>
|
||||
<IsOverlayContext.Provider value={true}>
|
||||
{overlayContent}
|
||||
</IsOverlayContext.Provider>
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</DndContext>
|
||||
</SortableInternalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SortableItemProps extends useRender.ComponentProps<"div"> {
|
||||
value: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function SortableItem({
|
||||
value,
|
||||
className,
|
||||
render,
|
||||
disabled,
|
||||
...props
|
||||
}: SortableItemProps) {
|
||||
const isOverlay = useContext(IsOverlayContext)
|
||||
|
||||
const {
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
attributes,
|
||||
listeners,
|
||||
isDragging: isSortableDragging,
|
||||
} = useSortable({
|
||||
id: value,
|
||||
disabled: disabled || isOverlay,
|
||||
animateLayoutChanges,
|
||||
})
|
||||
|
||||
const style = {
|
||||
transition,
|
||||
transform: CSS.Transform.toString(transform),
|
||||
} as CSSProperties
|
||||
|
||||
const defaultProps = isOverlay
|
||||
? {
|
||||
"data-slot": "sortable-item",
|
||||
"data-value": value,
|
||||
"data-dragging": true,
|
||||
className: cn(className),
|
||||
children: props.children,
|
||||
}
|
||||
: {
|
||||
"data-slot": "sortable-item",
|
||||
"data-value": value,
|
||||
"data-dragging": isSortableDragging,
|
||||
"data-disabled": disabled,
|
||||
ref: setNodeRef,
|
||||
style,
|
||||
...attributes,
|
||||
className: cn(
|
||||
isSortableDragging && "opacity-50 z-50",
|
||||
disabled && "opacity-50",
|
||||
className
|
||||
),
|
||||
children: props.children,
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableItemContext.Provider
|
||||
value={
|
||||
isOverlay
|
||||
? { listeners: undefined, isDragging: true, disabled: false }
|
||||
: { listeners, isDragging: isSortableDragging, disabled }
|
||||
}
|
||||
>
|
||||
{useRender({
|
||||
defaultTagName: "div",
|
||||
render,
|
||||
props: mergeProps<"div">(defaultProps, props),
|
||||
})}
|
||||
</SortableItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SortableItemHandleProps extends useRender.ComponentProps<"div"> {
|
||||
cursor?: boolean
|
||||
}
|
||||
|
||||
function SortableItemHandle({
|
||||
className,
|
||||
render,
|
||||
cursor = true,
|
||||
...props
|
||||
}: SortableItemHandleProps) {
|
||||
const { listeners, isDragging, disabled } = useContext(SortableItemContext)
|
||||
|
||||
const defaultProps = {
|
||||
"data-slot": "sortable-item-handle",
|
||||
"data-dragging": isDragging,
|
||||
"data-disabled": disabled,
|
||||
...listeners,
|
||||
className: cn(
|
||||
cursor && (isDragging ? "cursor-grabbing!" : "cursor-grab!"),
|
||||
className
|
||||
),
|
||||
children: props.children,
|
||||
}
|
||||
|
||||
return useRender({
|
||||
defaultTagName: "div",
|
||||
render,
|
||||
props: mergeProps<"div">(defaultProps, props),
|
||||
})
|
||||
}
|
||||
|
||||
export interface SortableOverlayProps extends Omit<
|
||||
React.ComponentProps<typeof DragOverlay>,
|
||||
"children"
|
||||
> {
|
||||
children?: ReactNode | ((params: { value: UniqueIdentifier }) => ReactNode)
|
||||
}
|
||||
|
||||
function SortableOverlay({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: SortableOverlayProps) {
|
||||
const { activeId, modifiers } = useContext(SortableInternalContext)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useLayoutEffect(() => setMounted(true), [])
|
||||
|
||||
const content =
|
||||
activeId && children
|
||||
? typeof children === "function"
|
||||
? children({ value: activeId })
|
||||
: children
|
||||
: null
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<DragOverlay
|
||||
dropAnimation={dropAnimationConfig}
|
||||
modifiers={modifiers}
|
||||
className={cn("z-50", activeId && "cursor-grabbing", className)}
|
||||
{...props}
|
||||
>
|
||||
<IsOverlayContext.Provider value={true}>
|
||||
{content}
|
||||
</IsOverlayContext.Provider>
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
export { Sortable, SortableItem, SortableItemHandle, SortableOverlay }
|
||||
Reference in New Issue
Block a user