studio/src/components/member-groups/group-resource-selector.tsx (195 lines of code) (raw):
import { Popover, PopoverTrigger } from "@/components/ui/popover";
import {
UpdateOrganizationGroupRequest_GroupRule,
GetUserAccessibleResourcesResponse,
} from "@wundergraph/cosmo-connect/dist/platform/v1/platform_pb";
import { roles } from "@/lib/constants";
import { useMemo, useState } from "react";
import { PopoverContentWithScrollableContent } from "../popover-content-with-scrollable-content";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ChevronRightIcon, CheckIcon, MinusIcon } from "@heroicons/react/24/outline";
import { useGroupResources, GroupResource, GroupResourceItem } from "./use-group-resources";
import { RiLoader5Fill } from "react-icons/ri";
export function GroupResourceSelector({ rule, disabled, activeRole, accessibleResources, onRuleUpdated }: {
rule: UpdateOrganizationGroupRequest_GroupRule,
disabled: boolean;
activeRole: (typeof roles[number]) | undefined;
accessibleResources: GetUserAccessibleResourcesResponse | undefined;
onRuleUpdated(rule: UpdateOrganizationGroupRequest_GroupRule): void;
}) {
const availableResources = useGroupResources({ rule, activeRole, accessibleResources });
const toggleResources = (resources: string[], isNamespaceResource: boolean) => {
const newRule = rule.clone();
const setOfSelectedResources = new Set(isNamespaceResource ? rule.namespaces : rule.resources);
for (const res of Array.from(new Set(resources))) {
if (setOfSelectedResources.has(res)) {
setOfSelectedResources.delete(res);
} else {
setOfSelectedResources.add(res);
}
}
if (isNamespaceResource) {
newRule.namespaces = Array.from(setOfSelectedResources);
} else {
newRule.resources = Array.from(setOfSelectedResources);
}
onRuleUpdated(newRule);
};
const selectedResources = rule.namespaces.length + rule.resources.length;
return accessibleResources ? (
<Popover>
<PopoverTrigger asChild>
<Button
variant="link"
className="px-0 justify-start grow truncate"
disabled={disabled}
>
<span className="truncate">
{selectedResources === 0
? "Grants access to all resources."
: `${selectedResources} resource(s) selected`}
</span>
</Button>
</PopoverTrigger>
<PopoverContentWithScrollableContent className="p-1 text-sm w-[400px]">
<div className="max-h-[32rem] overflow-auto">
{availableResources.length > 0
? availableResources.map((res, index) => (
<GroupSelectorItem
key={`resource-${index}`}
depth={0}
toggleResources={toggleResources}
{...res}
/>
))
: (
<div className="p-2 text-center text-muted-foreground">No resources available</div>
)}
</div>
</PopoverContentWithScrollableContent>
</Popover>
) : (
<div className="flex justify-start items-center grow truncate h-9 text-sm gap-x-2">
<RiLoader5Fill className="size-4 animate-spin" />
<span>Loading resources...</span>
</div>
);
}
function flatten(children: GroupResourceItem[]): GroupResourceItem[] {
const result: GroupResourceItem[] = [];
for (const child of children) {
if (!child.children) {
result.push(child);
continue;
} else if (!child.children || child.children.length === 0) {
continue;
}
result.push(...flatten(child.children));
}
return result;
}
function GroupSelectorItem({ type, label, children, depth, toggleResources, ...rest }: GroupResource & {
depth: number;
toggleResources(res: string[], isNamespaceResource: boolean) : void;
}) {
const [expanded, setExpanded] = useState(false);
const flattenChildren = useMemo(() => flatten(children ?? []), [children]);
if (children && children.length === 0) {
return null;
}
if (type === "segment") {
return (
<>
<div className="text-xs text-muted-foreground p-1.5 uppercase select-none">
{label}
</div>
{children?.map((child) => (
<GroupSelectorItem
key={child.value}
depth={depth}
toggleResources={toggleResources}
{...child}
/>
))}
</>
);
}
const { value, isNamespaceResource, disabled, selected } = rest as GroupResourceItem;
const hasChildren = children && children.length > 0;
const isExpanded = expanded;
const hasSelectedSomeChildren = hasChildren && flattenChildren.some((c) => c.selected);
const hasSelectedEveryChildren = hasChildren && flattenChildren.every((c) => c.selected);
return (
<>
<div
className={cn(
"flex justify-start items-center gap-x-1.5 px-2.5 py-1.5 hover:bg-accent rounded select-none w-full group/item",
disabled && "opacity-50 cursor-not-allowed hover:bg-transparent"
)}
role="button"
onClick={() => {
if (disabled) {
return;
}
if (hasChildren) {
setExpanded(!expanded);
} else {
toggleResources([value], isNamespaceResource);
}
}}
>
{hasChildren ? (
<ChevronRightIcon
className={cn("size-3 transition-all duration-200 shrink-0", isExpanded && "rotate-90")}
/>
) : depth > 0 && <span className="w-4 shrink-0" /> }
<span
className={cn("group/check shrink-0", disabled && "pointer-events-none")}
role="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (disabled) {
return;
}
if (hasChildren) {
if (hasSelectedEveryChildren) {
toggleResources(flattenChildren.map((res) => res.value), isNamespaceResource);
} else {
toggleResources(
flattenChildren.filter((res) => !res.selected).map((res) => res.value),
isNamespaceResource
);
}
} else {
toggleResources([value], isNamespaceResource);
}
}}
>
<span
className={cn(
"flex justify-center items-center size-5 border border-border rounded transition-all duration-200",
selected || hasSelectedSomeChildren
? "bg-primary"
: "bg-popover hover:bg-accent group-hover/item:bg-popover group-hover/check:bg-gray-500/30"
)}
>
{(selected || hasSelectedEveryChildren) ? (
<CheckIcon className="size-3" />
) : hasSelectedSomeChildren ? (
<MinusIcon className="size-3" />
) : null}
</span>
</span>
<span className="truncate grow">
{label}
</span>
</div>
{children && children.length > 0 && isExpanded && (
<div className="pl-[18px]">
{children.map((child) => (
<GroupSelectorItem key={child.value} {...child} depth={depth + 1} toggleResources={toggleResources} />
))}
</div>)}
</>
);
}