Compare commits

...

4 Commits

Author SHA1 Message Date
Eduard Gert
d498e4cc25 Fix dns records pagination (#566)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2026-02-20 21:42:26 +01:00
Eduard Gert
130dc0c32c Fix group unused filter (#565)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2026-02-19 10:32:14 +01:00
Eduard Gert
f5824d6ddb Allow empty groups for reverse proxy sso auth (#563)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2026-02-18 16:27:13 +01:00
Eduard Gert
829395f908 Add hover to reverse proxy auth methods (#564) 2026-02-18 13:39:19 +01:00
10 changed files with 189 additions and 52 deletions

View File

@@ -6,24 +6,26 @@ export const ListItem = ({
label,
value,
className,
children,
}: {
icon?: React.ReactNode;
label: string;
value: string | React.ReactNode;
className?: string;
children?: React.ReactNode;
}) => {
return (
<div
className={cn(
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
className,
)}
className={cn(" border-b border-nb-gray-920 last:border-b-0", className)}
>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
<div className={cn("flex justify-between gap-12 py-2 px-4")}>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
</div>
<div className={"text-nb-gray-300"}>{value}</div>
</div>
<div className={"text-nb-gray-300"}>{value}</div>
{children}
</div>
);
};

View File

@@ -76,7 +76,7 @@ interface MultiSelectProps {
closeOnSelect?: boolean;
resource?: PolicyRuleResource;
onResourceChange?: (resource?: PolicyRuleResource) => void;
placeholder?: string;
placeholder?: React.ReactNode | string;
customTrigger?: React.ReactNode;
align?: "start" | "end";
side?: "top" | "bottom";
@@ -397,7 +397,9 @@ export function PeerGroupSelector({
})}
{values.length == 0 && !resource && (
<span className={"pl-1"}>{placeholder}</span>
<span className={cn(typeof placeholder === "string" && "pl-1")}>
{placeholder}
</span>
)}
</div>

View File

@@ -28,14 +28,30 @@ const UserProfileContext = React.createContext(
);
export default function UsersProvider({ children }: Readonly<Props>) {
const { data: users, mutate, isLoading } = useFetchApi<User[]>("/users");
const { data: users, mutate, isLoading } = useFetchApi<User[]>(
"/users?service_user=false",
);
const { data: serviceUsers, mutate: mutateServiceUsers, isLoading: isLoadingServiceUsers } = useFetchApi<
User[]
>("/users?service_user=true");
const refresh = () => {
mutate().then();
mutateServiceUsers().then();
};
const allUsers = useMemo(() => {
return [...(users ?? []), ...(serviceUsers ?? [])];
}, [users, serviceUsers]);
return (
<UsersContext.Provider value={{ users, refresh, isLoading }}>
<UsersContext.Provider
value={{
users: allUsers,
refresh,
isLoading: isLoading || isLoadingServiceUsers,
}}
>
<UserProfileProvider>{children}</UserProfileProvider>
</UsersContext.Provider>
);

View File

@@ -66,6 +66,7 @@ export default function DNSRecordsTable({ zone }: Props) {
className={"bg-nb-gray-960 py-2"}
inset={true}
text={"DNS Records"}
initialPageSize={zone?.records?.length}
manualPagination={true}
sorting={sorting}
columnVisibility={{}}

View File

@@ -240,7 +240,7 @@ export const GroupsTableColumns: ColumnDef<GroupUsage>[] = [
row.setup_keys_count > 0 ||
row.users_count > 0 ||
row.resources_count > 0 ||
row.zones_count
row.zones_count > 0
);
},
},

View File

@@ -791,6 +791,7 @@ export default function ReverseProxyModal({
onOpenChange={setSsoModalOpen}
key={ssoModalOpen ? "sso1" : "sso0"}
currentGroups={bearerGroups}
isEnabled={bearerEnabled}
onSave={(groups) => {
setTimeout(() => {
setBearerGroups(groups);

View File

@@ -5,11 +5,15 @@ import { PeerGroupSelector } from "@components/PeerGroupSelector";
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
import React, { useState } from "react";
import { Group } from "@/interfaces/Group";
import { useUsers } from "@/contexts/UsersProvider";
import Badge from "@components/Badge";
import { CircleUser } from "lucide-react";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
currentGroups: Group[];
isEnabled: boolean;
onSave: (groups: Group[]) => void;
onRemove: () => void;
};
@@ -18,17 +22,17 @@ export default function AuthSSOModal({
open,
onOpenChange,
currentGroups,
isEnabled,
onSave,
onRemove,
}: Readonly<Props>) {
const { users } = useUsers();
const [groups, setGroups] = useState<Group[]>(currentGroups);
const isEditing = currentGroups.length > 0;
const isEditing = isEnabled;
const handleSave = () => {
if (groups.length > 0) {
onOpenChange(false);
onSave(groups);
}
onOpenChange(false);
onSave(groups);
};
const handleRemove = () => {
@@ -51,7 +55,17 @@ export default function AuthSSOModal({
<PeerGroupSelector
values={groups}
onChange={setGroups}
placeholder="Select distribution groups..."
placeholder={
<div className={"flex items-center gap-2"}>
<Badge className={"py-[3px]"} variant={"gray-ghost"}>
<CircleUser size={12} />
All Users
</Badge>
Select user groups...
</div>
}
users={users}
hideAllGroup={true}
/>
<div className="flex gap-3 w-full justify-between mt-6">
{isEditing ? (
@@ -63,11 +77,7 @@ export default function AuthSSOModal({
<ModalClose asChild>
<Button variant="secondary">Cancel</Button>
</ModalClose>
<Button
variant="primary"
onClick={handleSave}
disabled={groups.length === 0}
>
<Button variant="primary" onClick={handleSave}>
Save
</Button>
</div>
@@ -79,12 +89,8 @@ export default function AuthSSOModal({
<ModalClose asChild>
<Button variant="secondary">Cancel</Button>
</ModalClose>
<Button
variant="primary"
onClick={handleSave}
disabled={groups.length === 0}
>
Add Groups
<Button variant="primary" onClick={handleSave}>
Add SSO
</Button>
</div>
</>

View File

@@ -1,11 +1,51 @@
import Badge from "@components/Badge";
import Button from "@components/Button";
import { Settings, ShieldCheck, ShieldOff } from "lucide-react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@components/HoverCard";
import { ListItem } from "@components/ListItem";
import GroupBadge from "@components/ui/GroupBadge";
import { UserCountStack } from "@components/ui/MultipleGroups";
import {
ArrowRightIcon,
Binary,
LucideIcon,
RectangleEllipsis,
Settings,
ShieldCheck,
ShieldOff,
Users,
} from "lucide-react";
import * as React from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { useReverseProxies } from "@/contexts/ReverseProxiesProvider";
import { Group } from "@/interfaces/Group";
import { ReverseProxy } from "@/interfaces/ReverseProxy";
const AUTH_METHODS: {
key: "password_auth" | "pin_auth" | "bearer_auth";
label: string;
hoverLabel: string;
Icon: LucideIcon;
}[] = [
{
key: "password_auth",
label: "Password",
hoverLabel: "Password",
Icon: RectangleEllipsis,
},
{ key: "pin_auth", label: "PIN Code", hoverLabel: "PIN Code", Icon: Binary },
{
key: "bearer_auth",
label: "SSO",
hoverLabel: "SSO (Single Sign On)",
Icon: Users,
},
];
type Props = {
reverseProxy: ReverseProxy;
};
@@ -15,14 +55,36 @@ export default function ReverseProxyAuthCell({
}: Readonly<Props>) {
const { permission } = usePermissions();
const { openModal } = useReverseProxies();
const { groups } = useGroups();
const auth = reverseProxy.auth;
const enabledCount = [
auth?.bearer_auth?.enabled,
auth?.link_auth?.enabled,
auth?.password_auth?.enabled,
auth?.pin_auth?.enabled,
].filter(Boolean).length;
const enabled = AUTH_METHODS.filter((m) => auth?.[m.key]?.enabled);
const ssoGroups = auth?.bearer_auth?.enabled
? (auth.bearer_auth.distribution_groups ?? [])
.map((groupId) => groups?.find((g) => g.id === groupId))
.filter((g): g is Group => g != undefined)
: [];
const showHoverContent =
enabled.length > 1 || (enabled.length === 1 && auth?.bearer_auth?.enabled);
const SingleIcon = enabled.length === 1 ? enabled[0].Icon : null;
const badgeContent =
SingleIcon ? (
<>
<SingleIcon size={12} className="text-green-500" />
<span className={"font-medium text-xs"}>{enabled[0].label}</span>
</>
) : enabled.length > 1 ? (
<>
<ShieldCheck size={12} className="text-green-400" />
<span className={"font-medium text-xs"}>
{enabled.length} Enabled
</span>
</>
) : null;
return (
<div
@@ -32,19 +94,66 @@ export default function ReverseProxyAuthCell({
openModal({ proxy: reverseProxy, initialTab: "auth" });
}}
>
{enabledCount > 0 ? (
<Badge variant={"gray"} useHover={false} className={"cursor-pointer"}>
<ShieldCheck size={12} className="text-green-400" />
<div>
<span className={"font-medium text-xs"}>Enabled</span>
</div>
</Badge>
) : (
<Badge variant={"gray"}>
<ShieldOff size={12} className="text-red-500" />
<span className={"font-medium text-xs"}>None</span>
</Badge>
)}
<HoverCard openDelay={200} closeDelay={100}>
<HoverCardTrigger asChild={true}>
{badgeContent ? (
<Badge
variant={"gray"}
useHover={false}
className={"cursor-pointer"}
>
{badgeContent}
</Badge>
) : (
<Badge variant={"gray"}>
<ShieldOff size={12} className="text-red-500" />
<span className={"font-medium text-xs"}>None</span>
</Badge>
)}
</HoverCardTrigger>
{showHoverContent && (
<HoverCardContent
className={"p-0"}
sideOffset={14}
onClick={(e) => e.stopPropagation()}
>
<div className={"text-xs"}>
{enabled.map(({ key, hoverLabel, Icon }) => (
<ListItem
key={key}
className={"py-0.5"}
icon={<Icon size={14} />}
label={hoverLabel}
value={
<div className={"text-green-500"}>
{key === "bearer_auth" && ssoGroups.length === 0
? "All Users"
: "Enabled"}
</div>
}
>
{key === "bearer_auth" && ssoGroups.length > 0 && (
<div className={"flex flex-col gap-2 px-4 pt-2 pb-3"}>
{ssoGroups.map((group) => (
<div
key={group.id}
className={
"flex gap-2 items-center justify-between"
}
>
<GroupBadge group={group} />
<ArrowRightIcon size={14} />
<UserCountStack group={group} />
</div>
))}
</div>
)}
</ListItem>
))}
</div>
</HoverCardContent>
)}
</HoverCard>
<Button
size={"xs"}

View File

@@ -75,7 +75,7 @@ const ReverseProxyColumns: ColumnDef<ReverseProxy>[] = [
{
accessorKey: "auth",
header: ({ column }) => {
return <DataTableHeader column={column}>Authentication</DataTableHeader>;
return <DataTableHeader column={column}>Auth Methods</DataTableHeader>;
},
cell: ({ row }) => <ReverseProxyAuthCell reverseProxy={row.original} />,
},

View File

@@ -104,7 +104,7 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
{
accessorKey: "auth",
header: ({ column }) => (
<DataTableHeader column={column}>Authentication</DataTableHeader>
<DataTableHeader column={column}>Auth Methods</DataTableHeader>
),
cell: ({ row }) => (
<ReverseProxyAuthCell reverseProxy={row.original.proxy} />