mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-11 23:04:59 +00:00
Update UI to support additions on the resource
This commit is contained in:
@@ -336,7 +336,10 @@ export default function ResourceAuthenticationPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<EditPolicyForm readonly />
|
<EditPolicyForm
|
||||||
|
readonly
|
||||||
|
resourceId={resource.resourceId}
|
||||||
|
/>
|
||||||
</ResourcePolicyProvider>
|
</ResourcePolicyProvider>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type MultiSelectTagsProps<T extends TagValue> = {
|
|||||||
onSearch: (query: string) => void;
|
onSearch: (query: string) => void;
|
||||||
ref?: Ref<HTMLButtonElement>;
|
ref?: Ref<HTMLButtonElement>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
lockedIds?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MultiSelectContent<T extends TagValue>({
|
export function MultiSelectContent<T extends TagValue>({
|
||||||
@@ -32,7 +33,8 @@ export function MultiSelectContent<T extends TagValue>({
|
|||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
onSearch,
|
onSearch,
|
||||||
onChange
|
onChange,
|
||||||
|
lockedIds
|
||||||
}: MultiSelectTagsProps<T>) {
|
}: MultiSelectTagsProps<T>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const selectedValues = new Set(value.map((v) => v.id));
|
const selectedValues = new Set(value.map((v) => v.id));
|
||||||
@@ -48,33 +50,38 @@ export function MultiSelectContent<T extends TagValue>({
|
|||||||
{emptyPlaceholder ?? t("noResults")}
|
{emptyPlaceholder ?? t("noResults")}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option) => (
|
{options.map((option) => {
|
||||||
<CommandItem
|
const isLocked = lockedIds?.has(option.id);
|
||||||
value={option.id}
|
return (
|
||||||
key={option.id}
|
<CommandItem
|
||||||
onSelect={() => {
|
value={option.id}
|
||||||
let newValues = [];
|
key={option.id}
|
||||||
if (selectedValues.has(option.id)) {
|
disabled={isLocked}
|
||||||
newValues = value.filter(
|
onSelect={() => {
|
||||||
(v) => v.id !== option.id
|
if (isLocked) return;
|
||||||
);
|
let newValues = [];
|
||||||
} else {
|
if (selectedValues.has(option.id)) {
|
||||||
newValues = [...value, option];
|
newValues = value.filter(
|
||||||
}
|
(v) => v.id !== option.id
|
||||||
onChange(newValues);
|
);
|
||||||
}}
|
} else {
|
||||||
>
|
newValues = [...value, option];
|
||||||
<CheckIcon
|
}
|
||||||
className={cn(
|
onChange(newValues);
|
||||||
"mr-2 h-4 w-4",
|
}}
|
||||||
selectedValues.has(option.id)
|
>
|
||||||
? "opacity-100"
|
<CheckIcon
|
||||||
: "opacity-0"
|
className={cn(
|
||||||
)}
|
"mr-2 h-4 w-4",
|
||||||
/>
|
selectedValues.has(option.id)
|
||||||
{`${option.text}`}
|
? "opacity-100"
|
||||||
</CommandItem>
|
: "opacity-0"
|
||||||
))}
|
)}
|
||||||
|
/>
|
||||||
|
{`${option.text}`}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
import { ChevronDownIcon, LockIcon, XIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
type MultiSelectTagsProps,
|
type MultiSelectTagsProps,
|
||||||
type TagValue,
|
type TagValue,
|
||||||
@@ -16,10 +16,12 @@ export interface MultiSelectInputProps<
|
|||||||
T extends TagValue
|
T extends TagValue
|
||||||
> extends MultiSelectTagsProps<T> {
|
> extends MultiSelectTagsProps<T> {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
lockedIds?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectTagInput<T extends TagValue>({
|
export function MultiSelectTagInput<T extends TagValue>({
|
||||||
buttonText,
|
buttonText,
|
||||||
|
lockedIds,
|
||||||
...props
|
...props
|
||||||
}: MultiSelectInputProps<T>) {
|
}: MultiSelectInputProps<T>) {
|
||||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
const selectedValues = new Set(props.value.map((v) => v.id));
|
||||||
@@ -52,46 +54,63 @@ export function MultiSelectTagInput<T extends TagValue>({
|
|||||||
"overflow-x-auto"
|
"overflow-x-auto"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{props.value.map((option) => (
|
{props.value.map((option) => {
|
||||||
<span
|
const isLocked = lockedIds?.has(option.id);
|
||||||
key={option.id}
|
return (
|
||||||
className={cn(
|
<span
|
||||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
key={option.id}
|
||||||
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
|
className={cn(
|
||||||
)}
|
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
||||||
onClick={(e) => e.stopPropagation()}
|
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5",
|
||||||
>
|
isLocked && "opacity-60"
|
||||||
{option.text}
|
)}
|
||||||
<button
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="p-0.5 flex-none cursor-pointer"
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
let newValues = [];
|
|
||||||
if (selectedValues.has(option.id)) {
|
|
||||||
newValues = props.value.filter(
|
|
||||||
(v) => v.id !== option.id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
newValues = [
|
|
||||||
...props.value,
|
|
||||||
option
|
|
||||||
];
|
|
||||||
}
|
|
||||||
props.onChange(newValues);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<XIcon className="size-3.5" />
|
{option.text}
|
||||||
</button>
|
{isLocked ? (
|
||||||
</span>
|
<span className="p-0.5 flex-none">
|
||||||
))}
|
<LockIcon className="size-3" />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="p-0.5 flex-none cursor-pointer"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let newValues = [];
|
||||||
|
if (
|
||||||
|
selectedValues.has(
|
||||||
|
option.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
newValues =
|
||||||
|
props.value.filter(
|
||||||
|
(v) =>
|
||||||
|
v.id !==
|
||||||
|
option.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newValues = [
|
||||||
|
...props.value,
|
||||||
|
option
|
||||||
|
];
|
||||||
|
}
|
||||||
|
props.onChange(newValues);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<span className="pl-1 font-normal">{buttonText}</span>
|
<span className="pl-1 font-normal">{buttonText}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
<ChevronDownIcon className="ml-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0">
|
<PopoverContent className="p-0">
|
||||||
<MultiSelectContent {...props} />
|
<MultiSelectContent {...props} lockedIds={lockedIds} />
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ import { EditPolicyRulesSectionForm } from "./EditPolicyRulesSectionForm";
|
|||||||
export type EditPolicyFormProps = {
|
export type EditPolicyFormProps = {
|
||||||
hidePolicyNameForm?: boolean;
|
hidePolicyNameForm?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
resourceId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EditPolicyForm({
|
export function EditPolicyForm({
|
||||||
hidePolicyNameForm,
|
hidePolicyNameForm,
|
||||||
readonly
|
readonly,
|
||||||
|
resourceId
|
||||||
}: EditPolicyFormProps) {
|
}: EditPolicyFormProps) {
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -84,6 +86,7 @@ export function EditPolicyForm({
|
|||||||
orgId={org.org.orgId}
|
orgId={org.org.orgId}
|
||||||
allIdps={allIdps}
|
allIdps={allIdps}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
|
resourceId={resourceId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditPolicyAuthMethodsSectionForm readonly={readonly} />
|
<EditPolicyAuthMethodsSectionForm readonly={readonly} />
|
||||||
@@ -97,6 +100,7 @@ export function EditPolicyForm({
|
|||||||
isMaxmindAvailable={isMaxmindAvailable}
|
isMaxmindAvailable={isMaxmindAvailable}
|
||||||
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
|
resourceId={resourceId}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,13 +75,28 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable
|
useReactTable
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react";
|
import {
|
||||||
|
ArrowUpDown,
|
||||||
|
Check,
|
||||||
|
ChevronsUpDown,
|
||||||
|
LockIcon,
|
||||||
|
Plus
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useCallback, useMemo, useState, useTransition } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useTransition
|
||||||
|
} from "react";
|
||||||
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
|
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
|
||||||
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { resourceQueries } from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
@@ -103,18 +118,21 @@ type LocalRule = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
new?: boolean;
|
new?: boolean;
|
||||||
updated?: boolean;
|
updated?: boolean;
|
||||||
|
fromPolicy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PolicyRulesSectionProps = {
|
type PolicyRulesSectionProps = {
|
||||||
isMaxmindAvailable: boolean;
|
isMaxmindAvailable: boolean;
|
||||||
isMaxmindAsnAvailable: boolean;
|
isMaxmindAsnAvailable: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
resourceId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EditPolicyRulesSectionForm({
|
export function EditPolicyRulesSectionForm({
|
||||||
isMaxmindAvailable,
|
isMaxmindAvailable,
|
||||||
isMaxmindAsnAvailable,
|
isMaxmindAsnAvailable,
|
||||||
readonly
|
readonly,
|
||||||
|
resourceId
|
||||||
}: PolicyRulesSectionProps) {
|
}: PolicyRulesSectionProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -122,6 +140,18 @@ export function EditPolicyRulesSectionForm({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isResourceOverlay = resourceId !== undefined;
|
||||||
|
|
||||||
|
// ── Fetch resource-specific rules when in overlay mode ───────────────────
|
||||||
|
const { data: resourceRulesData } = useQuery({
|
||||||
|
...resourceQueries.resourceRules({ resourceId: resourceId! }),
|
||||||
|
enabled: isResourceOverlay
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedResourceRuleIdsRef = useRef<Set<number>>(new Set());
|
||||||
|
const [resourceRulesInitialized, setResourceRulesInitialized] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
createPolicySchema.pick({
|
createPolicySchema.pick({
|
||||||
@@ -140,8 +170,42 @@ export function EditPolicyRulesSectionForm({
|
|||||||
name: "applyRules"
|
name: "applyRules"
|
||||||
});
|
});
|
||||||
|
|
||||||
const [rules, setRules] = useState<LocalRule[]>(policy.rules);
|
const [rules, setRules] = useState<LocalRule[]>(
|
||||||
const [isExpanded, setIsExpanded] = useState(rulesEnabled);
|
policy.rules.map((r) => ({ ...r, fromPolicy: !isResourceOverlay }))
|
||||||
|
);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(
|
||||||
|
rulesEnabled || isResourceOverlay
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize resource-specific rules once fetched
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isResourceOverlay || resourceRulesInitialized) return;
|
||||||
|
if (!resourceRulesData) return;
|
||||||
|
|
||||||
|
const policyRuleIds = new Set(policy.rules.map((r) => r.ruleId));
|
||||||
|
const resourceSpecific: LocalRule[] = resourceRulesData
|
||||||
|
.filter((r) => !policyRuleIds.has(r.ruleId))
|
||||||
|
.map((r) => ({
|
||||||
|
ruleId: r.ruleId,
|
||||||
|
action: r.action as "ACCEPT" | "DROP" | "PASS",
|
||||||
|
match: r.match,
|
||||||
|
value: r.value,
|
||||||
|
priority: r.priority,
|
||||||
|
enabled: r.enabled,
|
||||||
|
fromPolicy: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
setRules([
|
||||||
|
...policy.rules.map((r) => ({ ...r, fromPolicy: true })),
|
||||||
|
...resourceSpecific
|
||||||
|
]);
|
||||||
|
setResourceRulesInitialized(true);
|
||||||
|
}, [
|
||||||
|
isResourceOverlay,
|
||||||
|
resourceRulesData,
|
||||||
|
resourceRulesInitialized,
|
||||||
|
policy.rules
|
||||||
|
]);
|
||||||
|
|
||||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -275,11 +339,17 @@ export function EditPolicyRulesSectionForm({
|
|||||||
|
|
||||||
const removeRule = useCallback(
|
const removeRule = useCallback(
|
||||||
function removeRule(ruleId: number) {
|
function removeRule(ruleId: number) {
|
||||||
|
const rule = rules.find((r) => r.ruleId === ruleId);
|
||||||
|
if (!rule || rule.fromPolicy) return; // cannot remove policy rules
|
||||||
|
// Track deletion for resource overlay mode (only for existing DB rules)
|
||||||
|
if (isResourceOverlay && !rule.new) {
|
||||||
|
deletedResourceRuleIdsRef.current.add(ruleId);
|
||||||
|
}
|
||||||
const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId);
|
const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId);
|
||||||
setRules(updatedRules);
|
setRules(updatedRules);
|
||||||
syncFormRules(updatedRules);
|
syncFormRules(updatedRules);
|
||||||
},
|
},
|
||||||
[rules, syncFormRules]
|
[rules, syncFormRules, isResourceOverlay]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateRule = useCallback(
|
const updateRule = useCallback(
|
||||||
@@ -328,35 +398,45 @@ export function EditPolicyRulesSectionForm({
|
|||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Input
|
const isLocked = row.original.fromPolicy;
|
||||||
defaultValue={row.original.priority}
|
if (isLocked) {
|
||||||
className="w-[75px]"
|
return (
|
||||||
type="number"
|
<span className="px-3 text-muted-foreground">
|
||||||
disabled={readonly}
|
—
|
||||||
onClick={(e) => e.currentTarget.focus()}
|
</span>
|
||||||
onBlur={(e) => {
|
);
|
||||||
const parsed = z.coerce
|
}
|
||||||
.number()
|
return (
|
||||||
.int()
|
<Input
|
||||||
.optional()
|
defaultValue={row.original.priority}
|
||||||
.safeParse(e.target.value);
|
className="w-[75px]"
|
||||||
if (!parsed.success) {
|
type="number"
|
||||||
toast({
|
disabled={readonly}
|
||||||
variant: "destructive",
|
onClick={(e) => e.currentTarget.focus()}
|
||||||
title: t("rulesErrorInvalidPriority"),
|
onBlur={(e) => {
|
||||||
description: t(
|
const parsed = z.coerce
|
||||||
"rulesErrorInvalidPriorityDescription"
|
.number()
|
||||||
)
|
.int()
|
||||||
|
.optional()
|
||||||
|
.safeParse(e.target.value);
|
||||||
|
if (!parsed.success) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("rulesErrorInvalidPriority"),
|
||||||
|
description: t(
|
||||||
|
"rulesErrorInvalidPriorityDescription"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateRule(row.original.ruleId, {
|
||||||
|
priority: parsed.data
|
||||||
});
|
});
|
||||||
return;
|
}}
|
||||||
}
|
/>
|
||||||
updateRule(row.original.ruleId, {
|
);
|
||||||
priority: parsed.data
|
}
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "action",
|
accessorKey: "action",
|
||||||
@@ -364,7 +444,7 @@ export function EditPolicyRulesSectionForm({
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={row.original.action}
|
defaultValue={row.original.action}
|
||||||
disabled={readonly}
|
disabled={readonly || row.original.fromPolicy}
|
||||||
onValueChange={(value: "ACCEPT" | "DROP" | "PASS") =>
|
onValueChange={(value: "ACCEPT" | "DROP" | "PASS") =>
|
||||||
updateRule(row.original.ruleId, { action: value })
|
updateRule(row.original.ruleId, { action: value })
|
||||||
}
|
}
|
||||||
@@ -394,7 +474,7 @@ export function EditPolicyRulesSectionForm({
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Select
|
<Select
|
||||||
defaultValue={row.original.match}
|
defaultValue={row.original.match}
|
||||||
disabled={readonly}
|
disabled={readonly || row.original.fromPolicy}
|
||||||
onValueChange={(
|
onValueChange={(
|
||||||
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN"
|
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN"
|
||||||
) =>
|
) =>
|
||||||
@@ -444,7 +524,9 @@ export function EditPolicyRulesSectionForm({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
disabled={readonly}
|
disabled={
|
||||||
|
readonly || row.original.fromPolicy
|
||||||
|
}
|
||||||
className="min-w-50 justify-between"
|
className="min-w-50 justify-between"
|
||||||
>
|
>
|
||||||
{row.original.value
|
{row.original.value
|
||||||
@@ -500,7 +582,9 @@ export function EditPolicyRulesSectionForm({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
disabled={readonly}
|
disabled={
|
||||||
|
readonly || row.original.fromPolicy
|
||||||
|
}
|
||||||
className="min-w-50 justify-between"
|
className="min-w-50 justify-between"
|
||||||
>
|
>
|
||||||
{row.original.value
|
{row.original.value
|
||||||
@@ -586,7 +670,7 @@ export function EditPolicyRulesSectionForm({
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={row.original.value}
|
defaultValue={row.original.value}
|
||||||
className="min-w-50"
|
className="min-w-50"
|
||||||
disabled={readonly}
|
disabled={readonly || row.original.fromPolicy}
|
||||||
onBlur={(e) =>
|
onBlur={(e) =>
|
||||||
updateRule(row.original.ruleId, {
|
updateRule(row.original.ruleId, {
|
||||||
value: e.target.value
|
value: e.target.value
|
||||||
@@ -601,7 +685,7 @@ export function EditPolicyRulesSectionForm({
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={row.original.enabled}
|
defaultChecked={row.original.enabled}
|
||||||
disabled={readonly}
|
disabled={readonly || row.original.fromPolicy}
|
||||||
onCheckedChange={(val) =>
|
onCheckedChange={(val) =>
|
||||||
updateRule(row.original.ruleId, { enabled: val })
|
updateRule(row.original.ruleId, { enabled: val })
|
||||||
}
|
}
|
||||||
@@ -613,13 +697,23 @@ export function EditPolicyRulesSectionForm({
|
|||||||
header: () => <span className="p-3">{t("actions")}</span>,
|
header: () => <span className="p-3">{t("actions")}</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
{row.original.fromPolicy ? (
|
||||||
variant="outline"
|
<Button
|
||||||
disabled={readonly}
|
variant="outline"
|
||||||
onClick={() => removeRule(row.original.ruleId)}
|
disabled
|
||||||
>
|
className="cursor-not-allowed"
|
||||||
{t("delete")}
|
>
|
||||||
</Button>
|
<LockIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={readonly}
|
||||||
|
onClick={() => removeRule(row.original.ruleId)}
|
||||||
|
>
|
||||||
|
{t("delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -651,11 +745,15 @@ export function EditPolicyRulesSectionForm({
|
|||||||
async function saveRules() {
|
async function saveRules() {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
|
|
||||||
|
if (isResourceOverlay) {
|
||||||
|
await saveResourceOverlayRules();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isValid = form.trigger();
|
const isValid = form.trigger();
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
|
|
||||||
const payload = form.getValues();
|
const payload = form.getValues();
|
||||||
console.log({ payload });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -689,6 +787,57 @@ export function EditPolicyRulesSectionForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveResourceOverlayRules() {
|
||||||
|
try {
|
||||||
|
const newRules = rules.filter((r) => !r.fromPolicy && r.new);
|
||||||
|
const updatedRules = rules.filter(
|
||||||
|
(r) => !r.fromPolicy && !r.new && r.updated
|
||||||
|
);
|
||||||
|
const deletedIds = [...deletedResourceRuleIdsRef.current];
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...newRules.map((r) =>
|
||||||
|
api.put(`/resource/${resourceId}/rule`, {
|
||||||
|
action: r.action,
|
||||||
|
match: r.match,
|
||||||
|
value: r.value,
|
||||||
|
priority: r.priority,
|
||||||
|
enabled: r.enabled
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...updatedRules.map((r) =>
|
||||||
|
api.post(`/resource/${resourceId}/rule/${r.ruleId}`, {
|
||||||
|
action: r.action,
|
||||||
|
match: r.match,
|
||||||
|
value: r.value,
|
||||||
|
priority: r.priority,
|
||||||
|
enabled: r.enabled
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...deletedIds.map((id) =>
|
||||||
|
api.delete(`/resource/${resourceId}/rule/${id}`)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
deletedResourceRuleIdsRef.current = new Set();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("policyUpdatedSuccess")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("policyErrorUpdate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("policyErrorUpdateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isExpanded) {
|
if (!isExpanded) {
|
||||||
return (
|
return (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
@@ -740,7 +889,7 @@ export function EditPolicyRulesSectionForm({
|
|||||||
onCheckedChange={(val) => {
|
onCheckedChange={(val) => {
|
||||||
form.setValue("applyRules", val);
|
form.setValue("applyRules", val);
|
||||||
}}
|
}}
|
||||||
disabled={readonly}
|
disabled={readonly || isResourceOverlay}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -763,7 +912,8 @@ export function EditPolicyRulesSectionForm({
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
disabled={
|
disabled={
|
||||||
readonly ||
|
readonly ||
|
||||||
!rulesEnabled
|
(!isResourceOverlay &&
|
||||||
|
!rulesEnabled)
|
||||||
}
|
}
|
||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
@@ -802,7 +952,8 @@ export function EditPolicyRulesSectionForm({
|
|||||||
value={field.value}
|
value={field.value}
|
||||||
disabled={
|
disabled={
|
||||||
readonly ||
|
readonly ||
|
||||||
!rulesEnabled
|
(!isResourceOverlay &&
|
||||||
|
!rulesEnabled)
|
||||||
}
|
}
|
||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
@@ -872,7 +1023,8 @@ export function EditPolicyRulesSectionForm({
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
disabled={
|
disabled={
|
||||||
readonly ||
|
readonly ||
|
||||||
!rulesEnabled
|
(!isResourceOverlay &&
|
||||||
|
!rulesEnabled)
|
||||||
}
|
}
|
||||||
aria-expanded={
|
aria-expanded={
|
||||||
openAddRuleCountrySelect
|
openAddRuleCountrySelect
|
||||||
@@ -965,7 +1117,8 @@ export function EditPolicyRulesSectionForm({
|
|||||||
role="combobox"
|
role="combobox"
|
||||||
disabled={
|
disabled={
|
||||||
readonly ||
|
readonly ||
|
||||||
!rulesEnabled
|
(!isResourceOverlay &&
|
||||||
|
!rulesEnabled)
|
||||||
}
|
}
|
||||||
aria-expanded={
|
aria-expanded={
|
||||||
openAddRuleAsnSelect
|
openAddRuleAsnSelect
|
||||||
@@ -1083,7 +1236,8 @@ export function EditPolicyRulesSectionForm({
|
|||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
readonly ||
|
readonly ||
|
||||||
!rulesEnabled
|
(!isResourceOverlay &&
|
||||||
|
!rulesEnabled)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1095,7 +1249,10 @@ export function EditPolicyRulesSectionForm({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={readonly || !rulesEnabled}
|
disabled={
|
||||||
|
readonly ||
|
||||||
|
(!isResourceOverlay && !rulesEnabled)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("ruleSubmit")}
|
{t("ruleSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ import {
|
|||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
|
|
||||||
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
||||||
import { useActionState, useState } from "react";
|
import { resourceQueries } from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useActionState, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useForm, useWatch } from "react-hook-form";
|
import { useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
// ─── PolicyUsersRolesSection ──────────────────────────────────────────────────
|
// ─── PolicyUsersRolesSection ──────────────────────────────────────────────────
|
||||||
@@ -54,12 +56,14 @@ type PolicyUsersRolesSectionProps = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
allIdps: { id: number; text: string }[];
|
allIdps: { id: number; text: string }[];
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
resourceId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EditPolicyUsersRolesSectionForm({
|
export function EditPolicyUsersRolesSectionForm({
|
||||||
orgId,
|
orgId,
|
||||||
allIdps,
|
allIdps,
|
||||||
readonly
|
readonly,
|
||||||
|
resourceId
|
||||||
}: PolicyUsersRolesSectionProps) {
|
}: PolicyUsersRolesSectionProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -69,6 +73,105 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
// ── Resource overlay: fetch resource-specific roles & users ──────────────
|
||||||
|
const isResourceOverlay = resourceId !== undefined;
|
||||||
|
|
||||||
|
const { data: resourceRolesData } = useQuery({
|
||||||
|
...resourceQueries.resourceRoles({ resourceId: resourceId! }),
|
||||||
|
enabled: isResourceOverlay
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: resourceUsersData } = useQuery({
|
||||||
|
...resourceQueries.resourceUsers({ resourceId: resourceId! }),
|
||||||
|
enabled: isResourceOverlay
|
||||||
|
});
|
||||||
|
|
||||||
|
// IDs from the policy (locked — cannot be removed)
|
||||||
|
const policyRoleLockedIds = useMemo(
|
||||||
|
() => new Set(policy.roles.map((r) => r.roleId.toString())),
|
||||||
|
[policy.roles]
|
||||||
|
);
|
||||||
|
const policyUserLockedIds = useMemo(
|
||||||
|
() => new Set(policy.users.map((u) => u.userId)),
|
||||||
|
[policy.users]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Policy entries mapped to selector format
|
||||||
|
const policyRoleItems = useMemo(
|
||||||
|
() =>
|
||||||
|
policy.roles.map((r) => ({
|
||||||
|
id: r.roleId.toString(),
|
||||||
|
text: r.name
|
||||||
|
})),
|
||||||
|
[policy.roles]
|
||||||
|
);
|
||||||
|
const policyUserItems = useMemo(
|
||||||
|
() =>
|
||||||
|
policy.users.map((u) => ({
|
||||||
|
id: u.userId,
|
||||||
|
text: `${getUserDisplayName({ email: u.email, username: u.username })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}`
|
||||||
|
})),
|
||||||
|
[policy.users]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track the initial resource-specific roles/users for diffing on save
|
||||||
|
const initialResourceRoleIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
const initialResourceUserIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Combined selected roles/users (policy + resource-specific)
|
||||||
|
const [combinedRoles, setCombinedRoles] = useState(policyRoleItems);
|
||||||
|
const [combinedUsers, setCombinedUsers] = useState(policyUserItems);
|
||||||
|
const [resourceRolesInitialized, setResourceRolesInitialized] =
|
||||||
|
useState(false);
|
||||||
|
const [resourceUsersInitialized, setResourceUsersInitialized] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isResourceOverlay || resourceRolesInitialized) return;
|
||||||
|
if (!resourceRolesData) return;
|
||||||
|
|
||||||
|
const resourceSpecific = resourceRolesData
|
||||||
|
.filter((r) => !policyRoleLockedIds.has(r.roleId.toString()))
|
||||||
|
.map((r) => ({ id: r.roleId.toString(), text: r.name }));
|
||||||
|
|
||||||
|
initialResourceRoleIdsRef.current = new Set(
|
||||||
|
resourceSpecific.map((r) => r.id)
|
||||||
|
);
|
||||||
|
setCombinedRoles([...policyRoleItems, ...resourceSpecific]);
|
||||||
|
setResourceRolesInitialized(true);
|
||||||
|
}, [
|
||||||
|
isResourceOverlay,
|
||||||
|
resourceRolesData,
|
||||||
|
resourceRolesInitialized,
|
||||||
|
policyRoleItems,
|
||||||
|
policyRoleLockedIds
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isResourceOverlay || resourceUsersInitialized) return;
|
||||||
|
if (!resourceUsersData) return;
|
||||||
|
|
||||||
|
const resourceSpecific = resourceUsersData
|
||||||
|
.filter((u) => !policyUserLockedIds.has(u.userId))
|
||||||
|
.map((u) => ({
|
||||||
|
id: u.userId,
|
||||||
|
text: `${getUserDisplayName({ email: u.email ?? undefined, username: u.username ?? undefined })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
initialResourceUserIdsRef.current = new Set(
|
||||||
|
resourceSpecific.map((u) => u.id)
|
||||||
|
);
|
||||||
|
setCombinedUsers([...policyUserItems, ...resourceSpecific]);
|
||||||
|
setResourceUsersInitialized(true);
|
||||||
|
}, [
|
||||||
|
isResourceOverlay,
|
||||||
|
resourceUsersData,
|
||||||
|
resourceUsersInitialized,
|
||||||
|
policyUserItems,
|
||||||
|
policyUserLockedIds
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Standard policy form (non-overlay) ──────────────────────────────────
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
createPolicySchema.pick({
|
createPolicySchema.pick({
|
||||||
@@ -81,14 +184,8 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
sso: policy.sso,
|
sso: policy.sso,
|
||||||
skipToIdpId: policy.idpId,
|
skipToIdpId: policy.idpId,
|
||||||
roles: policy.roles.map((role) => ({
|
roles: policyRoleItems,
|
||||||
id: role.roleId.toString(),
|
users: policyUserItems
|
||||||
text: role.name
|
|
||||||
})),
|
|
||||||
users: policy.users.map((user) => ({
|
|
||||||
id: user.userId,
|
|
||||||
text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}`
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,12 +196,17 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||||
|
const [isSavingOverlay, setIsSavingOverlay] = useState(false);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
|
|
||||||
const isValid = await form.trigger();
|
if (isResourceOverlay) {
|
||||||
|
await saveResourceOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await form.trigger();
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
|
|
||||||
const payload = form.getValues();
|
const payload = form.getValues();
|
||||||
@@ -147,6 +249,87 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveResourceOverlay() {
|
||||||
|
setIsSavingOverlay(true);
|
||||||
|
try {
|
||||||
|
// Compute which roles/users are resource-specific (non-locked)
|
||||||
|
const currentResourceRoleIds = new Set(
|
||||||
|
combinedRoles
|
||||||
|
.filter((r) => !policyRoleLockedIds.has(r.id))
|
||||||
|
.map((r) => r.id)
|
||||||
|
);
|
||||||
|
const currentResourceUserIds = new Set(
|
||||||
|
combinedUsers
|
||||||
|
.filter((u) => !policyUserLockedIds.has(u.id))
|
||||||
|
.map((u) => u.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialRoleIds = initialResourceRoleIdsRef.current;
|
||||||
|
const initialUserIds = initialResourceUserIdsRef.current;
|
||||||
|
|
||||||
|
const addedRoleIds = [...currentResourceRoleIds].filter(
|
||||||
|
(id) => !initialRoleIds.has(id)
|
||||||
|
);
|
||||||
|
const removedRoleIds = [...initialRoleIds].filter(
|
||||||
|
(id) => !currentResourceRoleIds.has(id)
|
||||||
|
);
|
||||||
|
const addedUserIds = [...currentResourceUserIds].filter(
|
||||||
|
(id) => !initialUserIds.has(id)
|
||||||
|
);
|
||||||
|
const removedUserIds = [...initialUserIds].filter(
|
||||||
|
(id) => !currentResourceUserIds.has(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...addedRoleIds.map((id) =>
|
||||||
|
api.post(`/resource/${resourceId}/roles/add`, {
|
||||||
|
roleId: Number(id)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...removedRoleIds.map((id) =>
|
||||||
|
api.post(`/resource/${resourceId}/roles/remove`, {
|
||||||
|
roleId: Number(id)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...addedUserIds.map((id) =>
|
||||||
|
api.post(`/resource/${resourceId}/users/add`, {
|
||||||
|
userId: id
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...removedUserIds.map((id) =>
|
||||||
|
api.post(`/resource/${resourceId}/users/remove`, {
|
||||||
|
userId: id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update refs to reflect new state
|
||||||
|
initialResourceRoleIdsRef.current = currentResourceRoleIds;
|
||||||
|
initialResourceUserIdsRef.current = currentResourceUserIds;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("policyUpdatedSuccess")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("policyErrorUpdate"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("policyErrorUpdateDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSavingOverlay(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isResourceOverlay &&
|
||||||
|
(!resourceRolesInitialized || !resourceUsersInitialized);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form action={formAction}>
|
<form action={formAction}>
|
||||||
@@ -166,78 +349,105 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
label={t("ssoUse")}
|
label={t("ssoUse")}
|
||||||
defaultChecked={ssoEnabled}
|
defaultChecked={ssoEnabled}
|
||||||
onCheckedChange={(val) => {
|
onCheckedChange={(val) => {
|
||||||
console.log(`form.setValue("sso", ${val})`);
|
|
||||||
form.setValue("sso", val);
|
form.setValue("sso", val);
|
||||||
}}
|
}}
|
||||||
disabled={readonly}
|
disabled={readonly || isResourceOverlay}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ssoEnabled && (
|
{ssoEnabled && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormItem className="flex flex-col items-start">
|
||||||
control={form.control}
|
<FormLabel>{t("roles")}</FormLabel>
|
||||||
name="roles"
|
<FormControl>
|
||||||
render={({ field }) => (
|
{isResourceOverlay ? (
|
||||||
<FormItem className="flex flex-col items-start">
|
<RolesSelector
|
||||||
<FormLabel>
|
orgId={orgId}
|
||||||
{t("roles")}
|
selectedRoles={
|
||||||
</FormLabel>
|
combinedRoles
|
||||||
<FormControl>
|
}
|
||||||
<RolesSelector
|
onSelectRoles={
|
||||||
orgId={orgId}
|
setCombinedRoles
|
||||||
selectedRoles={
|
}
|
||||||
field.value
|
disabled={isLoading}
|
||||||
}
|
restrictAdminRole
|
||||||
onSelectRoles={(
|
lockedIds={
|
||||||
roles
|
policyRoleLockedIds
|
||||||
) =>
|
}
|
||||||
form.setValue(
|
/>
|
||||||
"roles",
|
) : (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="roles"
|
||||||
|
render={({ field }) => (
|
||||||
|
<RolesSelector
|
||||||
|
orgId={orgId}
|
||||||
|
selectedRoles={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onSelectRoles={(
|
||||||
roles
|
roles
|
||||||
)
|
) =>
|
||||||
}
|
form.setValue(
|
||||||
disabled={readonly}
|
"roles",
|
||||||
restrictAdminRole
|
roles
|
||||||
/>
|
)
|
||||||
</FormControl>
|
}
|
||||||
<FormMessage />
|
disabled={readonly}
|
||||||
<FormDescription>
|
restrictAdminRole
|
||||||
{t(
|
/>
|
||||||
"resourceRoleDescription"
|
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
/>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
<FormField
|
<FormDescription>
|
||||||
control={form.control}
|
{t("resourceRoleDescription")}
|
||||||
name="users"
|
</FormDescription>
|
||||||
render={({ field }) => (
|
</FormItem>
|
||||||
<FormItem className="flex flex-col items-start">
|
|
||||||
<FormLabel>
|
<FormItem className="flex flex-col items-start">
|
||||||
{t("users")}
|
<FormLabel>{t("users")}</FormLabel>
|
||||||
</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
{isResourceOverlay ? (
|
||||||
<UsersSelector
|
<UsersSelector
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
selectedUsers={
|
selectedUsers={
|
||||||
field.value
|
combinedUsers
|
||||||
}
|
}
|
||||||
onSelectUsers={(
|
onSelectUsers={
|
||||||
users
|
setCombinedUsers
|
||||||
) =>
|
}
|
||||||
form.setValue(
|
disabled={isLoading}
|
||||||
"users",
|
lockedIds={
|
||||||
|
policyUserLockedIds
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="users"
|
||||||
|
render={({ field }) => (
|
||||||
|
<UsersSelector
|
||||||
|
orgId={orgId}
|
||||||
|
selectedUsers={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onSelectUsers={(
|
||||||
users
|
users
|
||||||
)
|
) =>
|
||||||
}
|
form.setValue(
|
||||||
disabled={readonly}
|
"users",
|
||||||
/>
|
users
|
||||||
</FormControl>
|
)
|
||||||
<FormMessage />
|
}
|
||||||
</FormItem>
|
disabled={readonly}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -247,7 +457,7 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
{t("defaultIdentityProvider")}
|
{t("defaultIdentityProvider")}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
disabled={readonly}
|
disabled={readonly || isResourceOverlay}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === "none") {
|
if (value === "none") {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
@@ -302,8 +512,13 @@ export function EditPolicyUsersRolesSectionForm({
|
|||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isSubmitting}
|
loading={isSubmitting || isSavingOverlay}
|
||||||
disabled={readonly || isSubmitting}
|
disabled={
|
||||||
|
readonly ||
|
||||||
|
isSubmitting ||
|
||||||
|
isSavingOverlay ||
|
||||||
|
isLoading
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("resourceUsersRolesSubmit")}
|
{t("resourceUsersRolesSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type RolesSelectorProps = {
|
|||||||
restrictAdminRole?: boolean;
|
restrictAdminRole?: boolean;
|
||||||
mapRolesByName?: boolean;
|
mapRolesByName?: boolean;
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
lockedIds?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RolesSelector({
|
export function RolesSelector({
|
||||||
@@ -25,7 +26,8 @@ export function RolesSelector({
|
|||||||
disabled,
|
disabled,
|
||||||
restrictAdminRole,
|
restrictAdminRole,
|
||||||
mapRolesByName,
|
mapRolesByName,
|
||||||
buttonText
|
buttonText,
|
||||||
|
lockedIds
|
||||||
}: RolesSelectorProps) {
|
}: RolesSelectorProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [roleSearchQuery, setRoleSearchQuery] = useState("");
|
const [roleSearchQuery, setRoleSearchQuery] = useState("");
|
||||||
@@ -76,6 +78,7 @@ export function RolesSelector({
|
|||||||
value={selectedRoles}
|
value={selectedRoles}
|
||||||
onChange={onSelectRoles}
|
onChange={onSelectRoles}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
lockedIds={lockedIds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ export type UsersSelectorProps = {
|
|||||||
selectedUsers?: SelectedUser[];
|
selectedUsers?: SelectedUser[];
|
||||||
onSelectUsers: (users: SelectedUser[]) => void;
|
onSelectUsers: (users: SelectedUser[]) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
lockedIds?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UsersSelector({
|
export function UsersSelector({
|
||||||
orgId,
|
orgId,
|
||||||
selectedUsers = [],
|
selectedUsers = [],
|
||||||
onSelectUsers,
|
onSelectUsers,
|
||||||
disabled
|
disabled,
|
||||||
|
lockedIds
|
||||||
}: UsersSelectorProps) {
|
}: UsersSelectorProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [userSearchQuery, setUserSearchQuery] = useState("");
|
const [userSearchQuery, setUserSearchQuery] = useState("");
|
||||||
@@ -61,6 +63,7 @@ export function UsersSelector({
|
|||||||
value={selectedUsers}
|
value={selectedUsers}
|
||||||
onChange={onSelectUsers}
|
onChange={onSelectUsers}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
lockedIds={lockedIds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
ListResourceNamesResponse,
|
ListResourceNamesResponse,
|
||||||
ListResourcesResponse,
|
ListResourcesResponse,
|
||||||
ListResourceRolesResponse,
|
ListResourceRolesResponse,
|
||||||
|
ListResourceRulesResponse,
|
||||||
ListResourceUsersResponse
|
ListResourceUsersResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||||
@@ -641,6 +642,17 @@ export const resourceQueries = {
|
|||||||
return res.data.data.roles;
|
return res.data.data.roles;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
resourceRules: ({ resourceId }: { resourceId: number }) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["RESOURCES", resourceId, "RULES"] as const,
|
||||||
|
queryFn: async ({ signal, meta }) => {
|
||||||
|
const res = await meta!.api.get<
|
||||||
|
AxiosResponse<ListResourceRulesResponse>
|
||||||
|
>(`/resource/${resourceId}/rules`, { signal });
|
||||||
|
|
||||||
|
return res.data.data.rules;
|
||||||
|
}
|
||||||
|
}),
|
||||||
siteResourceUsers: ({ siteResourceId }: { siteResourceId: number }) =>
|
siteResourceUsers: ({ siteResourceId }: { siteResourceId: number }) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["SITE_RESOURCES", siteResourceId, "USERS"] as const,
|
queryKey: ["SITE_RESOURCES", siteResourceId, "USERS"] as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user