Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fb6e0b05 | ||
|
|
55c5525626 |
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { motion } from "framer-motion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -23,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center gap-4 font-medium transition-all [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
|
||||
"flex flex-1 items-center gap-4 font-medium [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -36,20 +37,41 @@ const AccordionTrigger = React.forwardRef<
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className=" pt-0">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
>(({ className, children }, ref) => {
|
||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = wrapperRef.current?.closest("[data-state]");
|
||||
if (!el) return;
|
||||
|
||||
const update = () => setIsOpen(el.getAttribute("data-state") === "open");
|
||||
update();
|
||||
|
||||
const observer = new MutationObserver(update);
|
||||
observer.observe(el, { attributes: true, attributeFilter: ["data-state"] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isOpen ? "auto" : 0,
|
||||
opacity: isOpen ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.15, ease: "easeOut" }}
|
||||
className={cn("overflow-hidden text-sm", className)}
|
||||
>
|
||||
<div className="pt-0">{children}</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
|
||||
@@ -85,6 +85,7 @@ interface MultiSelectProps {
|
||||
users?: User[];
|
||||
placeholderForSearch?: string;
|
||||
resourceIds?: string[];
|
||||
additionalResources?: NetworkResource[];
|
||||
policies?: Policy[];
|
||||
}
|
||||
export function PeerGroupSelector({
|
||||
@@ -117,12 +118,21 @@ export function PeerGroupSelector({
|
||||
users,
|
||||
placeholderForSearch = 'Search groups or add new group by pressing "Enter"...',
|
||||
resourceIds,
|
||||
additionalResources,
|
||||
policies,
|
||||
}: Readonly<MultiSelectProps>) {
|
||||
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
|
||||
const { data: fetchedResources, isLoading: isResourcesLoading } = useFetchApi<
|
||||
NetworkResource[]
|
||||
>("/networks/resources");
|
||||
|
||||
const resources = useMemo(() => {
|
||||
if (!additionalResources?.length) return fetchedResources;
|
||||
const additional = additionalResources.filter(
|
||||
(ar) => !fetchedResources?.some((r) => r.id === ar.id),
|
||||
);
|
||||
return [...(fetchedResources || []), ...additional];
|
||||
}, [fetchedResources, additionalResources]);
|
||||
|
||||
const { data: peers, isLoading: isPeersLoading } =
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const ModalOverlay = React.forwardRef<
|
||||
"bg-black/30 dark:bg-black/40 backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
style={{ scrollbarGutter: "stable both-edges" }}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -26,6 +26,7 @@ const PoliciesContext = React.createContext(
|
||||
createPoliciesForResource: (
|
||||
policies: Policy[],
|
||||
resource: NetworkResource,
|
||||
knownGroups?: Group[],
|
||||
) => Promise<void>;
|
||||
openEditPolicyModal: (policy: Policy, tab?: string) => void;
|
||||
deletePolicy: (policy: Policy, onSuccess?: () => void) => Promise<void>;
|
||||
@@ -39,7 +40,7 @@ const PoliciesContext = React.createContext(
|
||||
export default function PoliciesProvider({ children }: Props) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const request = useApiCall<Policy>("/policies");
|
||||
const { createOrUpdate: createOrUpdateGroup } = useGroups();
|
||||
const { createOrUpdate: createOrUpdateGroup, groups } = useGroups();
|
||||
const [policyModal, setPolicyModal] = useState(false);
|
||||
const [currentPolicy, setCurrentPolicy] = useState<Policy>();
|
||||
const [initialPolicyTab, setInitialPolicyTab] = useState("");
|
||||
@@ -49,28 +50,33 @@ export default function PoliciesProvider({ children }: Props) {
|
||||
const createPolicyForResource = async (
|
||||
policy: Policy,
|
||||
resource: NetworkResource,
|
||||
knownGroups?: Group[],
|
||||
) => {
|
||||
const rule = policy.rules[0];
|
||||
|
||||
const allGroups = [...(knownGroups || []), ...(groups || [])];
|
||||
const resolveGroup = async (g: Group | string): Promise<string> => {
|
||||
if (typeof g === "string") return g;
|
||||
if (g.id) return g.id;
|
||||
const existing = allGroups.find((eg) => eg.name === g.name);
|
||||
if (existing?.id) return existing.id;
|
||||
const created = await createOrUpdateGroup(g);
|
||||
return created.id!;
|
||||
};
|
||||
|
||||
const sources = await Promise.all(
|
||||
(rule.sources ?? []).map((g) => {
|
||||
if (typeof g === "string") return g;
|
||||
if (g.id) return g.id;
|
||||
return createOrUpdateGroup(g).then((r) => r.id);
|
||||
}),
|
||||
(rule.sources ?? []).map(resolveGroup),
|
||||
).then((ids) => ids.filter(Boolean) as string[]);
|
||||
|
||||
const hasGroups = resource.groups && resource.groups.length > 0;
|
||||
const destinations = rule.destinationResource
|
||||
? undefined
|
||||
: await Promise.all((rule.destinations ?? []).map(resolveGroup)).then(
|
||||
(ids) => ids.filter(Boolean) as string[],
|
||||
);
|
||||
|
||||
const destinations = hasGroups
|
||||
? await Promise.all(
|
||||
(resource.groups as (Group | string)[]).map((g) => {
|
||||
if (typeof g === "string") return g;
|
||||
if (g.id) return g.id;
|
||||
return createOrUpdateGroup(g).then((r) => r.id);
|
||||
}),
|
||||
).then((ids) => ids.filter(Boolean) as string[])
|
||||
: null;
|
||||
const destinationResource = rule.destinationResource
|
||||
? { id: resource.id, type: resource.type }
|
||||
: undefined;
|
||||
|
||||
return createPolicy({
|
||||
...policy,
|
||||
@@ -82,9 +88,7 @@ export default function PoliciesProvider({ children }: Props) {
|
||||
...rule,
|
||||
sources,
|
||||
destinations,
|
||||
destinationResource: hasGroups
|
||||
? undefined
|
||||
: { id: resource.id, type: resource.type },
|
||||
destinationResource,
|
||||
},
|
||||
],
|
||||
} as Policy);
|
||||
@@ -93,22 +97,17 @@ export default function PoliciesProvider({ children }: Props) {
|
||||
const createPoliciesForResource = async (
|
||||
newPolicies: Policy[],
|
||||
resource: NetworkResource,
|
||||
knownGroups?: Group[],
|
||||
) => {
|
||||
const policiesToCreate = newPolicies.filter((p) => !p.id);
|
||||
if (policiesToCreate.length === 0) return;
|
||||
|
||||
const promise = Promise.all(
|
||||
policiesToCreate.map((p) => createPolicyForResource(p, resource)),
|
||||
).then(() => mutate("/policies"));
|
||||
|
||||
notify({
|
||||
title: "Create Policies",
|
||||
description: "Successfully created policies for resource.",
|
||||
promise,
|
||||
showOnlyError: true,
|
||||
});
|
||||
|
||||
return promise;
|
||||
await Promise.all(
|
||||
policiesToCreate.map((p) =>
|
||||
createPolicyForResource(p, resource, knownGroups),
|
||||
),
|
||||
);
|
||||
await mutate("/policies");
|
||||
};
|
||||
|
||||
const serializeRules = (rules: Policy["rules"], enabled?: boolean) => {
|
||||
|
||||
@@ -46,6 +46,7 @@ import React, { useMemo, useState } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import { Policy, PolicyRuleResource, Protocol } from "@/interfaces/Policy";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { useAccessControl } from "@/modules/access-control/useAccessControl";
|
||||
@@ -126,6 +127,7 @@ type ModalProps = {
|
||||
initialDestinationResource?: PolicyRuleResource;
|
||||
initialTab?: string;
|
||||
disableDestinationSelector?: boolean;
|
||||
additionalResources?: NetworkResource[];
|
||||
};
|
||||
|
||||
export function AccessControlModalContent({
|
||||
@@ -143,6 +145,7 @@ export function AccessControlModalContent({
|
||||
initialDestinationResource,
|
||||
initialTab,
|
||||
disableDestinationSelector = false,
|
||||
additionalResources,
|
||||
}: Readonly<ModalProps>) {
|
||||
const { permission } = usePermissions();
|
||||
const { users } = useUsers();
|
||||
@@ -392,6 +395,7 @@ export function AccessControlModalContent({
|
||||
resource={destinationResource}
|
||||
onResourceChange={setDestinationResource}
|
||||
saveGroupAssignments={useSave}
|
||||
additionalResources={additionalResources}
|
||||
disabled={
|
||||
disableDestinationSelector ||
|
||||
!permission.policies.update ||
|
||||
|
||||
@@ -192,6 +192,7 @@ export const NetworkProvider = ({
|
||||
? [...fetchedResources, additionalResource]
|
||||
: fetchedResources;
|
||||
const isMulti = affectedResources.length > 1;
|
||||
if (!isMulti && action === "edit") return true;
|
||||
return confirm({
|
||||
title: isMulti ? (
|
||||
<>This policy is used by multiple resources</>
|
||||
@@ -206,8 +207,8 @@ export const NetworkProvider = ({
|
||||
action === "edit" ? "Updating" : "Deleting"
|
||||
} this policy will also affect following resources:`
|
||||
: action === "delete"
|
||||
? "Are you sure you want to delete this policy? This action cannot be undone."
|
||||
: undefined,
|
||||
? "Are you sure you want to delete this policy? This action cannot be undone."
|
||||
: undefined,
|
||||
children: isMulti ? (
|
||||
<AffectedResourceList resources={affectedResources} />
|
||||
) : undefined,
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function NetworkResourceAccessControl({
|
||||
|
||||
const currentResource = useMemo<NetworkResource>(() => {
|
||||
return {
|
||||
id: resourceId || "",
|
||||
id: resourceId || resourceName || address,
|
||||
name: resourceName || address,
|
||||
address,
|
||||
type: getResourceType(address),
|
||||
@@ -289,7 +289,8 @@ export default function NetworkResourceAccessControl({
|
||||
editingPolicyIndex === null ? destinationResource : undefined
|
||||
}
|
||||
disableDestinationSelector={!hasResourceGroups}
|
||||
initialName={`${resourceName || address} Policy`}
|
||||
additionalResources={[currentResource]}
|
||||
initialName={`${resourceName || address} Access`}
|
||||
initialDescription={
|
||||
network?.description
|
||||
? `${network.name}, ${network.description}`
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink, { InlineButtonLink } from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
@@ -26,11 +25,15 @@ import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
PlusCircle,
|
||||
Power,
|
||||
ShieldCheck,
|
||||
Text,
|
||||
WorkflowIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@components/Accordion";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { Network, NetworkResource } from "@/interfaces/Network";
|
||||
@@ -133,9 +136,7 @@ export function ResourceModalContent({
|
||||
return allPolicies.filter((policy) => {
|
||||
const rule = policy.rules?.[0];
|
||||
if (!rule || rule.destinationResource) return false;
|
||||
const destinations = rule.destinations as
|
||||
| (Group | string)[]
|
||||
| undefined;
|
||||
const destinations = rule.destinations as (Group | string)[] | undefined;
|
||||
return destinations?.some((d) => {
|
||||
const id = typeof d === "string" ? d : d.id;
|
||||
return !!id && groupIds.has(id);
|
||||
@@ -175,7 +176,7 @@ export function ResourceModalContent({
|
||||
groups: savedGroups ? savedGroups.map((g) => g.id) : undefined,
|
||||
enabled,
|
||||
}).then(async (r) => {
|
||||
await createPoliciesForResource(policies, r);
|
||||
await createPoliciesForResource(policies, r, savedGroups);
|
||||
onCreated?.(r);
|
||||
});
|
||||
|
||||
@@ -199,7 +200,7 @@ export function ResourceModalContent({
|
||||
groups: savedGroups ? savedGroups.map((g) => g.id) : undefined,
|
||||
enabled,
|
||||
}).then(async (r) => {
|
||||
await createPoliciesForResource(policies, r);
|
||||
await createPoliciesForResource(policies, r, savedGroups);
|
||||
onUpdated?.(r);
|
||||
});
|
||||
notify({
|
||||
@@ -217,7 +218,7 @@ export function ResourceModalContent({
|
||||
return (
|
||||
<ModalContent
|
||||
maxWidthClass={
|
||||
tab === "access-control" ? "max-w-[790px]" : "max-w-[720px]"
|
||||
tab === "access-control" ? "max-w-[790px]" : "max-w-[680px]"
|
||||
}
|
||||
>
|
||||
<ModalHeader
|
||||
@@ -239,32 +240,34 @@ export function ResourceModalContent({
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={"access-control"}
|
||||
disabled={!resource && !isAddressValid}
|
||||
disabled={!resource && !canCreate}
|
||||
>
|
||||
<ShieldCheck size={16} />
|
||||
Access Control
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value={"general"}
|
||||
disabled={!resource && !isAddressValid}
|
||||
>
|
||||
<Text
|
||||
size={16}
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Name & Description
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={"resource"} className={"pb-8"}>
|
||||
<div className={"px-8 flex-col flex gap-8"}>
|
||||
<TabsContent value={"resource"} className={"pb-4"}>
|
||||
<div className={"px-8 flex-col flex gap-6"}>
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<HelpText>
|
||||
Set an easily identifiable name for your resource
|
||||
</HelpText>
|
||||
<Input
|
||||
ref={nameRef}
|
||||
autoFocus={true}
|
||||
tabIndex={0}
|
||||
placeholder={"e.g., Postgres Database"}
|
||||
value={name}
|
||||
error={nameError}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<ResourceSingleAddressInput
|
||||
value={address}
|
||||
onChange={setAddress}
|
||||
onError={setAddressError}
|
||||
autoFocus={true}
|
||||
description={
|
||||
<>
|
||||
Enter a single{" "}
|
||||
@@ -295,59 +298,83 @@ export function ResourceModalContent({
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label>Resource Groups (optional)</Label>
|
||||
<HelpText>
|
||||
Organize this resource into a group (e.g., Databases, Web
|
||||
Servers) and reference the group in access policies to keep
|
||||
rules reusable and easy to maintain.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
side={"top"}
|
||||
onChange={setGroups}
|
||||
values={groups}
|
||||
showPeerCounter={false}
|
||||
placeholder={"Add or select resource group(s)..."}
|
||||
policies={allPolicies}
|
||||
/>
|
||||
{groupPolicyCount > 0 && (
|
||||
<Callout variant={"info"} className={"mt-3"}>
|
||||
Your selected resource groups are used in{" "}
|
||||
<span className="text-white font-medium">
|
||||
{groupPolicyCount} Access Control{" "}
|
||||
{groupPolicyCount === 1 ? "Policy" : "Policies"}
|
||||
<Accordion
|
||||
type={"multiple"}
|
||||
className={"flex flex-col gap-2 -mt-2"}
|
||||
>
|
||||
<AccordionItem value={"resource-groups"}>
|
||||
<AccordionTrigger
|
||||
className={
|
||||
"text-[0.8rem] tracking-wider text-nb-gray-200 py-4 my-0 leading-none gap-2 flex items-center"
|
||||
}
|
||||
>
|
||||
<span className={"relative top-[1px]"}>
|
||||
Additional Options (optional)
|
||||
</span>
|
||||
. This resource will inherit access from{" "}
|
||||
{groupPolicyCount === 1 ? "this policy" : "these policies"}.
|
||||
{isAddressValid || resource ? (
|
||||
<>
|
||||
{" "}
|
||||
Please review them in the{" "}
|
||||
<InlineButtonLink
|
||||
onClick={() => setTab("access-control")}
|
||||
variant={"dashed"}
|
||||
>
|
||||
Access Control
|
||||
</InlineButtonLink>{" "}
|
||||
tab.
|
||||
</>
|
||||
) : (
|
||||
" Please review them in the Access Control tab."
|
||||
)}
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
value={enabled}
|
||||
onChange={setEnabled}
|
||||
label={
|
||||
<>
|
||||
<Power size={15} />
|
||||
Enable Resource
|
||||
</>
|
||||
}
|
||||
helpText={"Use this switch to enable or disable the resource."}
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className={""}>
|
||||
<div className={"flex flex-col gap-6 pb-4 pt-2"}>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<HelpText>
|
||||
Write a short description to add more context to this
|
||||
resource.
|
||||
</HelpText>
|
||||
<Input
|
||||
placeholder={"e.g., Production, Development"}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Resource Groups</Label>
|
||||
<HelpText className={"mt-1"}>
|
||||
Add this resource to a group (e.g., Databases, Web
|
||||
Servers) and reference the group <br /> in access
|
||||
policies to simplify management.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
side={"top"}
|
||||
onChange={setGroups}
|
||||
values={groups}
|
||||
showPeerCounter={false}
|
||||
placeholder={"Add or select resource group(s)..."}
|
||||
policies={allPolicies}
|
||||
/>
|
||||
{groupPolicyCount > 0 && (
|
||||
<Callout variant={"info"} className={"mt-3"}>
|
||||
Your selected resource groups are used in{" "}
|
||||
<span className="text-white font-medium">
|
||||
{groupPolicyCount} Access Control{" "}
|
||||
{groupPolicyCount === 1 ? "Policy" : "Policies"}
|
||||
</span>
|
||||
. This resource will inherit access from{" "}
|
||||
{groupPolicyCount === 1
|
||||
? "this policy"
|
||||
: "these policies"}
|
||||
.
|
||||
{isAddressValid || resource ? (
|
||||
<>
|
||||
{" "}
|
||||
Please review them in the{" "}
|
||||
<InlineButtonLink
|
||||
onClick={() => setTab("access-control")}
|
||||
variant={"dashed"}
|
||||
>
|
||||
Access Control
|
||||
</InlineButtonLink>{" "}
|
||||
tab.
|
||||
</>
|
||||
) : (
|
||||
" Please review them in the Access Control tab."
|
||||
)}
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -362,36 +389,6 @@ export function ResourceModalContent({
|
||||
hasResourceGroups={groups.length > 0}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"general"} className={"px-8 pb-8"}>
|
||||
<div className={"flex flex-col gap-6"}>
|
||||
<div>
|
||||
<Label>Name</Label>
|
||||
<HelpText>
|
||||
Set an easily identifiable name for your resource
|
||||
</HelpText>
|
||||
<Input
|
||||
ref={nameRef}
|
||||
tabIndex={0}
|
||||
placeholder={"e.g., Postgres Database"}
|
||||
value={name}
|
||||
error={nameError}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description (optional)</Label>
|
||||
<HelpText>
|
||||
Write a short description to add more context to this resource.
|
||||
</HelpText>
|
||||
<Input
|
||||
placeholder={"e.g., Production, Development"}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
@@ -418,7 +415,7 @@ export function ResourceModalContent({
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("access-control")}
|
||||
disabled={!isAddressValid}
|
||||
disabled={!canCreate}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
@@ -433,26 +430,6 @@ export function ResourceModalContent({
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => {
|
||||
setTab("general");
|
||||
setTimeout(() => nameRef.current?.focus(), 0);
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === "general" && (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
onClick={() => setTab("access-control")}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
data-cy={"submit-route"}
|
||||
|
||||
@@ -12,6 +12,9 @@ import { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
import { Network, NetworkResource } from "@/interfaces/Network";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
import { FolderGit2 } from "lucide-react";
|
||||
import Separator from "@components/Separator";
|
||||
|
||||
type ResourceGroupModalProps = {
|
||||
resource?: NetworkResource;
|
||||
@@ -56,6 +59,7 @@ const ResourceGroupModalContent = ({
|
||||
`/networks/${network?.id}/resources/${resource?.id}`,
|
||||
).put;
|
||||
|
||||
const { policies } = useNetworksContext();
|
||||
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
|
||||
initial: resource?.groups || [],
|
||||
});
|
||||
@@ -80,17 +84,21 @@ const ResourceGroupModalContent = ({
|
||||
<ModalHeader
|
||||
title={"Resource Groups"}
|
||||
description={
|
||||
"Organize this resource into a group (e.g., Databases, Web Servers) and reference the group in access policies to keep rules reusable and easy to maintain."
|
||||
"Add this resource to a group (e.g., Databases, Web Servers) and reference the group in access policies to simplify management."
|
||||
}
|
||||
icon={<FolderGit2 size={18} />}
|
||||
/>
|
||||
|
||||
<div className={"px-8 py-6 pt-0 flex flex-col gap-8"}>
|
||||
<Separator />
|
||||
|
||||
<div className={"px-8 py-6 pt-6 flex flex-col gap-8"}>
|
||||
<div>
|
||||
<PeerGroupSelector
|
||||
onChange={setGroups}
|
||||
values={groups}
|
||||
showPeerCounter={false}
|
||||
placeholder={"Add or select resource group(s)..."}
|
||||
policies={policies}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user