diff --git a/ui/changelog.d/rbac-unlimited-visibility.changed.md b/ui/changelog.d/rbac-unlimited-visibility.changed.md
new file mode 100644
index 00000000000..b8f5bc57250
--- /dev/null
+++ b/ui/changelog.d/rbac-unlimited-visibility.changed.md
@@ -0,0 +1 @@
+RBAC role forms now explain Unlimited Visibility inside the Visibility section and keep the setting visible while group selection is hidden
diff --git a/ui/components/roles/workflow/forms/add-role-form.test.tsx b/ui/components/roles/workflow/forms/add-role-form.test.tsx
index 80a9c523b39..561bf9bddf6 100644
--- a/ui/components/roles/workflow/forms/add-role-form.test.tsx
+++ b/ui/components/roles/workflow/forms/add-role-form.test.tsx
@@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { afterEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { AddRoleForm } from "./add-role-form";
@@ -76,6 +76,17 @@ vi.mock("@/components/ui", () => ({
useToast: () => ({ toast: vi.fn() }),
}));
+beforeAll(() => {
+ class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+
+ globalThis.ResizeObserver = ResizeObserverMock;
+ window.ResizeObserver = ResizeObserverMock;
+});
+
describe("AddRoleForm", () => {
afterEach(() => {
routerMocks.push.mockClear();
@@ -117,4 +128,207 @@ describe("AddRoleForm", () => {
// Then
expect(routerMocks.push).toHaveBeenCalledWith("/roles");
});
+
+ it("shows a subtle inline Unlimited Visibility description", () => {
+ // Given / When
+ render();
+
+ // Then
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ expect(
+ screen.getByText(/tenant-wide visibility setting/i),
+ ).toHaveTextContent(
+ /grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i,
+ );
+ expect(
+ screen.getByText(/required to use the Jira integration/i),
+ ).toHaveProperty("tagName", "STRONG");
+ expect(
+ screen.queryByRole("heading", { name: "Unlimited Visibility" }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ /does not grant admin actions such as managing users, providers, scans, integrations, billing, or alerts/i,
+ ),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ /enable it only for roles that need tenant-wide security visibility/i,
+ ),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ /manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i,
+ ),
+ ).not.toBeInTheDocument();
+
+ const visibilityHeading = screen.getByText("Visibility");
+ const unlimitedVisibilityCheckbox = screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ });
+
+ expect(
+ visibilityHeading.compareDocumentPosition(unlimitedVisibilityCheckbox) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+
+ it("keeps the Visibility section and hides only groups when Unlimited Visibility is enabled", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+
+ // Then
+ expect(screen.getByText("Visibility")).toBeInTheDocument();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).toBeChecked();
+ expect(
+ screen.getByText(/tenant-wide visibility setting/i),
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("group-select")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/select the groups this role will have access to/i),
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not force Unlimited Visibility when Manage Providers is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ expect(
+ screen.queryByText(
+ /Manage Providers is selected, so Unlimited Visibility stays enabled in this form/i,
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not force Unlimited Visibility when granting all admin permissions", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ });
+
+ it("keeps Unlimited Visibility user-controlled when Manage Providers is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ expect(screen.getByTestId("group-select")).toBeInTheDocument();
+ });
+
+ it("keeps explicitly enabled Unlimited Visibility when all admin permissions are toggled off", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).not.toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).toBeChecked();
+ });
+
+ it("does not show extra Manage Providers guidance for explicitly enabled Unlimited Visibility", async () => {
+ // Given
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ );
+
+ // Then
+ expect(
+ screen.queryByText(
+ /Manage Providers is selected, so Unlimited Visibility stays enabled in this form/i,
+ ),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/remove this automatic visibility grant/i),
+ ).not.toBeInTheDocument();
+ });
});
diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx
index 2ae5db944d1..c02311c0f25 100644
--- a/ui/components/roles/workflow/forms/add-role-form.tsx
+++ b/ui/components/roles/workflow/forms/add-role-form.tsx
@@ -1,85 +1,38 @@
"use client";
-import { Checkbox } from "@heroui/checkbox";
-import { Divider } from "@heroui/divider";
-import { Tooltip } from "@heroui/tooltip";
-import { zodResolver } from "@hookform/resolvers/zod";
-import clsx from "clsx";
-import { InfoIcon } from "lucide-react";
import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { z } from "zod";
+import { DefaultValues } from "react-hook-form";
import { addRole } from "@/actions/roles/roles";
-import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { useToast } from "@/components/ui";
-import { CustomInput } from "@/components/ui/custom";
-import { Form, FormButtons } from "@/components/ui/form";
-import { getErrorMessage, permissionFormFields } from "@/lib";
-import { addRoleFormSchema, ApiError } from "@/types";
+import { getErrorMessage } from "@/lib";
+import { RoleFormValues } from "@/types";
-type FormValues = z.input;
+import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
-export const AddRoleForm = ({
- groups,
-}: {
- groups: { id: string; name: string }[];
-}) => {
+export const AddRoleForm = ({ groups }: { groups: RoleGroupOption[] }) => {
const { toast } = useToast();
const router = useRouter();
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
- const visiblePermissionFormFields = permissionFormFields.filter(
- (permission) =>
- !["manage_billing", "manage_alerts"].includes(permission.field) ||
- isCloudEnvironment,
- );
-
- const form = useForm({
- resolver: zodResolver(addRoleFormSchema),
- defaultValues: {
- name: "",
- manage_users: false,
- manage_providers: false,
- manage_integrations: false,
- manage_scans: false,
- unlimited_visibility: false,
- groups: [],
- ...(isCloudEnvironment && {
- manage_billing: false,
- manage_alerts: false,
- }),
- },
- });
-
- const { watch, setValue } = form;
- const manageProviders = watch("manage_providers");
- const unlimitedVisibility = watch("unlimited_visibility");
-
- useEffect(() => {
- if (manageProviders && !unlimitedVisibility) {
- setValue("unlimited_visibility", true, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- });
- }
- }, [manageProviders, unlimitedVisibility, setValue]);
-
- const isLoading = form.formState.isSubmitting;
-
- const onSelectAllChange = (checked: boolean) => {
- visiblePermissionFormFields.forEach(({ field }) => {
- form.setValue(field as keyof FormValues, checked, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- });
- });
+ const defaultValues: DefaultValues = {
+ name: "",
+ manage_users: false,
+ manage_providers: false,
+ manage_integrations: false,
+ manage_scans: false,
+ unlimited_visibility: false,
+ groups: [],
+ ...(isCloudEnvironment && {
+ manage_billing: false,
+ manage_alerts: false,
+ }),
};
- const onSubmitClient = async (values: FormValues) => {
+ const onSubmit = async (
+ values: RoleFormValues,
+ { handleServerResponse }: RoleFormSubmitContext,
+ ) => {
const formData = new FormData();
formData.append("name", values.name);
@@ -107,33 +60,13 @@ export const AddRoleForm = ({
try {
const data = await addRole(formData);
+ if (!handleServerResponse(data)) return;
- if (data?.errors && data.errors.length > 0) {
- data.errors.forEach((error: ApiError) => {
- const errorMessage = error.detail;
- const pointer = error.source?.pointer;
- switch (pointer) {
- case "/data/attributes/name":
- form.setError("name", {
- type: "server",
- message: errorMessage,
- });
- break;
- default:
- toast({
- variant: "destructive",
- title: "Error",
- description: errorMessage,
- });
- }
- });
- } else {
- toast({
- title: "Role Added",
- description: "The role was added successfully.",
- });
- router.push("/roles");
- }
+ toast({
+ title: "Role Added",
+ description: "The role was added successfully.",
+ });
+ router.push("/roles");
} catch (error) {
toast({
variant: "destructive",
@@ -144,120 +77,11 @@ export const AddRoleForm = ({
};
return (
-
-
+
);
};
diff --git a/ui/components/roles/workflow/forms/edit-role-form.test.tsx b/ui/components/roles/workflow/forms/edit-role-form.test.tsx
new file mode 100644
index 00000000000..fe1dd23639c
--- /dev/null
+++ b/ui/components/roles/workflow/forms/edit-role-form.test.tsx
@@ -0,0 +1,326 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
+
+import { EditRoleForm } from "./edit-role-form";
+
+const routerMocks = vi.hoisted(() => ({
+ push: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => routerMocks,
+}));
+
+vi.mock("@/actions/roles/roles", () => ({
+ updateRole: vi.fn(),
+}));
+
+vi.mock("@/lib", () => ({
+ cn: (...classes: Array) =>
+ classes.filter(Boolean).join(" "),
+ getErrorMessage: (error: unknown) => String(error),
+ permissionFormFields: [
+ {
+ field: "manage_users",
+ label: "Invite and Manage Users",
+ description:
+ "Allows inviting new users and managing existing user details",
+ },
+ {
+ field: "manage_account",
+ label: "Manage Account",
+ description: "Provides access to account settings and RBAC configuration",
+ },
+ {
+ field: "unlimited_visibility",
+ label: "Unlimited Visibility",
+ description:
+ "Provides complete visibility across all the providers and its related resources",
+ },
+ {
+ field: "manage_providers",
+ label: "Manage Providers",
+ description:
+ "Allows configuration and management of provider connections",
+ },
+ {
+ field: "manage_integrations",
+ label: "Manage Integrations",
+ description:
+ "Allows configuration and management of third-party integrations",
+ },
+ {
+ field: "manage_scans",
+ label: "Manage Scans",
+ description: "Allows launching and configuring scans security scans",
+ },
+ {
+ field: "manage_alerts",
+ label: "Manage Alerts",
+ description: "Allows creating and managing custom alerts",
+ },
+ {
+ field: "manage_billing",
+ label: "Manage Billing",
+ description: "Provides access to billing settings and invoices",
+ },
+ ],
+}));
+
+vi.mock("@/components/shadcn/select/enhanced-multi-select", () => ({
+ EnhancedMultiSelect: () => ,
+}));
+
+vi.mock("@/components/ui", () => ({
+ useToast: () => ({ toast: vi.fn() }),
+}));
+
+beforeAll(() => {
+ class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+
+ globalThis.ResizeObserver = ResizeObserverMock;
+ window.ResizeObserver = ResizeObserverMock;
+});
+
+const roleData = ({
+ manageProviders = false,
+ unlimitedVisibility = false,
+}: {
+ manageProviders?: boolean;
+ unlimitedVisibility?: boolean;
+} = {}) => ({
+ data: {
+ attributes: {
+ name: "Existing role",
+ manage_users: false,
+ manage_account: false,
+ manage_providers: manageProviders,
+ manage_integrations: false,
+ manage_scans: false,
+ unlimited_visibility: unlimitedVisibility,
+ groups: [],
+ },
+ relationships: {
+ provider_groups: {
+ data: [],
+ },
+ },
+ },
+});
+
+const renderEditRoleForm = (options?: Parameters[0]) =>
+ render(
+ ,
+ );
+
+describe("EditRoleForm", () => {
+ afterEach(() => {
+ routerMocks.push.mockClear();
+ vi.unstubAllEnvs();
+ });
+
+ it("shows the subtle Unlimited Visibility description inside Visibility", () => {
+ // Given / When
+ renderEditRoleForm();
+
+ // Then
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+ expect(
+ screen.getByText(/tenant-wide visibility setting/i),
+ ).toHaveTextContent(
+ /grants visibility into every provider, account, resource, finding, scan, and compliance result.*required to use the Jira integration/i,
+ );
+ expect(
+ screen.getByText(/required to use the Jira integration/i),
+ ).toHaveProperty("tagName", "STRONG");
+ expect(
+ screen.queryByText(
+ /manage providers enables unlimited visibility in this form because provider administration needs tenant-wide provider-group context/i,
+ ),
+ ).not.toBeInTheDocument();
+
+ const visibilityHeading = screen.getByText("Visibility");
+ const unlimitedVisibilityCheckbox = screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ });
+
+ expect(
+ visibilityHeading.compareDocumentPosition(unlimitedVisibilityCheckbox) &
+ Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+
+ it("keeps the Visibility section and hides only groups when Unlimited Visibility is enabled", async () => {
+ // Given
+ const user = userEvent.setup();
+ render(
+ ,
+ );
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+
+ // Then
+ expect(screen.getByText("Visibility")).toBeInTheDocument();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).toBeChecked();
+ expect(
+ screen.getByText(/tenant-wide visibility setting/i),
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("group-select")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/select the groups this role will have access to/i),
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not force Unlimited Visibility when Manage Providers is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ renderEditRoleForm();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ expect(
+ screen.queryByText(
+ /Manage Providers is selected, so Unlimited Visibility stays enabled in this form/i,
+ ),
+ ).not.toBeInTheDocument();
+ });
+
+ it("does not force Unlimited Visibility when granting all admin permissions", async () => {
+ // Given
+ const user = userEvent.setup();
+ renderEditRoleForm();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ });
+
+ it("keeps Unlimited Visibility user-controlled when Manage Providers is selected", async () => {
+ // Given
+ const user = userEvent.setup();
+ renderEditRoleForm();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).not.toBeChecked();
+ expect(screen.getByTestId("group-select")).toBeInTheDocument();
+ });
+
+ it("keeps explicitly enabled Unlimited Visibility when all admin permissions are toggled off", async () => {
+ // Given
+ const user = userEvent.setup();
+ renderEditRoleForm();
+
+ // When
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+ await user.click(
+ screen.getByRole("checkbox", { name: "Grant all admin permissions" }),
+ );
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).not.toBeChecked();
+ expect(
+ screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ }),
+ ).toBeChecked();
+ });
+
+ it("does not show extra Manage Providers guidance", () => {
+ // Given / When
+ renderEditRoleForm({ manageProviders: true, unlimitedVisibility: true });
+
+ // Then
+ expect(
+ screen.queryByText(
+ /Manage Providers is selected, so Unlimited Visibility stays enabled in this form/i,
+ ),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/remove this automatic visibility grant/i),
+ ).not.toBeInTheDocument();
+ });
+
+ it("keeps existing inconsistent Manage Providers and Unlimited Visibility values on initial render", () => {
+ // Given / When
+ renderEditRoleForm({ manageProviders: true, unlimitedVisibility: false });
+
+ // Then
+ expect(
+ screen.getByRole("checkbox", { name: "Manage Providers" }),
+ ).toBeChecked();
+ const unlimitedVisibilityCheckbox = screen.getByRole("checkbox", {
+ name: "Enable Unlimited Visibility for this role",
+ });
+ expect(unlimitedVisibilityCheckbox).not.toBeChecked();
+ expect(unlimitedVisibilityCheckbox).toBeEnabled();
+ });
+});
diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx
index e3e1a3bde86..4ba474b9434 100644
--- a/ui/components/roles/workflow/forms/edit-role-form.tsx
+++ b/ui/components/roles/workflow/forms/edit-role-form.tsx
@@ -1,25 +1,14 @@
"use client";
-import { Checkbox } from "@heroui/checkbox";
-import { Divider } from "@heroui/divider";
-import { Tooltip } from "@heroui/tooltip";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { clsx } from "clsx";
-import { InfoIcon } from "lucide-react";
import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import { Controller, useForm } from "react-hook-form";
-import { z } from "zod";
+import { DefaultValues } from "react-hook-form";
import { updateRole } from "@/actions/roles/roles";
-import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { useToast } from "@/components/ui";
-import { CustomInput } from "@/components/ui/custom";
-import { Form, FormButtons } from "@/components/ui/form";
-import { getErrorMessage, permissionFormFields } from "@/lib";
-import { ApiError, editRoleFormSchema } from "@/types";
+import { getErrorMessage } from "@/lib";
+import { RoleFormValues } from "@/types";
-type FormValues = z.input;
+import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
export const EditRoleForm = ({
roleId,
@@ -29,7 +18,7 @@ export const EditRoleForm = ({
roleId: string;
roleData: {
data: {
- attributes: FormValues;
+ attributes: RoleFormValues;
relationships?: {
provider_groups?: {
data: Array<{ id: string; type: string }>;
@@ -37,57 +26,24 @@ export const EditRoleForm = ({
};
};
};
- groups: { id: string; name: string }[];
+ groups: RoleGroupOption[];
}) => {
const { toast } = useToast();
const router = useRouter();
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
- const visiblePermissionFormFields = permissionFormFields.filter(
- (permission) =>
- !["manage_billing", "manage_alerts"].includes(permission.field) ||
- isCloudEnvironment,
- );
-
- const form = useForm({
- resolver: zodResolver(editRoleFormSchema),
- defaultValues: {
- ...roleData.data.attributes,
- groups:
- roleData.data.relationships?.provider_groups?.data.map((g) => g.id) ||
- [],
- },
- });
-
- const { watch, setValue } = form;
-
- const manageProviders = watch("manage_providers");
- const unlimitedVisibility = watch("unlimited_visibility");
-
- useEffect(() => {
- if (manageProviders && !unlimitedVisibility) {
- setValue("unlimited_visibility", true, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- });
- }
- }, [manageProviders, unlimitedVisibility, setValue]);
-
- const isLoading = form.formState.isSubmitting;
- const onSelectAllChange = (checked: boolean) => {
- visiblePermissionFormFields.forEach(({ field }) => {
- form.setValue(field as keyof FormValues, checked, {
- shouldValidate: true,
- shouldDirty: true,
- shouldTouch: true,
- });
- });
+ const defaultValues: DefaultValues = {
+ ...roleData.data.attributes,
+ groups:
+ roleData.data.relationships?.provider_groups?.data.map((g) => g.id) || [],
};
- const onSubmitClient = async (values: FormValues) => {
+ const onSubmit = async (
+ values: RoleFormValues,
+ { handleServerResponse }: RoleFormSubmitContext,
+ ) => {
try {
- const updatedFields: Partial = {};
+ const updatedFields: Partial = {};
if (values.name !== roleData.data.attributes.name) {
updatedFields.name = values.name;
@@ -127,33 +83,13 @@ export const EditRoleForm = ({
});
const data = await updateRole(formData, roleId);
+ if (!handleServerResponse(data)) return;
- if (data?.errors && data.errors.length > 0) {
- data.errors.forEach((error: ApiError) => {
- const errorMessage = error.detail;
- const pointer = error.source?.pointer;
- switch (pointer) {
- case "/data/attributes/name":
- form.setError("name", {
- type: "server",
- message: errorMessage,
- });
- break;
- default:
- toast({
- variant: "destructive",
- title: "Error",
- description: errorMessage,
- });
- }
- });
- } else {
- toast({
- title: "Role Updated",
- description: "The role was updated successfully.",
- });
- router.push("/roles");
- }
+ toast({
+ title: "Role Updated",
+ description: "The role was updated successfully.",
+ });
+ router.push("/roles");
} catch (error) {
toast({
variant: "destructive",
@@ -164,119 +100,11 @@ export const EditRoleForm = ({
};
return (
-
-
+
);
};
diff --git a/ui/components/roles/workflow/forms/role-form.tsx b/ui/components/roles/workflow/forms/role-form.tsx
new file mode 100644
index 00000000000..6704cc7b2bf
--- /dev/null
+++ b/ui/components/roles/workflow/forms/role-form.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { InfoIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import {
+ Controller,
+ DefaultValues,
+ useForm,
+ UseFormReturn,
+} from "react-hook-form";
+
+import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
+import { Input } from "@/components/shadcn/input/input";
+import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
+import { Separator } from "@/components/shadcn/separator/separator";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
+import {
+ Form,
+ FormButtons,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { useFormServerErrors } from "@/hooks/use-form-server-errors";
+import { useManageProvidersUnlimitedVisibility } from "@/hooks/use-manage-providers-unlimited-visibility";
+import {
+ getUnlimitedVisibilityField,
+ getVisiblePermissionFormFields,
+} from "@/lib/role-permissions";
+import { roleFormSchema, RoleFormValues } from "@/types";
+
+import { UnlimitedVisibilityField } from "./unlimited-visibility-section";
+
+export interface RoleGroupOption {
+ id: string;
+ name: string;
+}
+
+export interface RoleFormSubmitContext {
+ form: UseFormReturn;
+ handleServerResponse: (data: unknown) => boolean;
+}
+
+interface RoleFormProps {
+ groups: RoleGroupOption[];
+ defaultValues: DefaultValues;
+ submitText: string;
+ onSubmit: (
+ values: RoleFormValues,
+ ctx: RoleFormSubmitContext,
+ ) => void | Promise;
+ onCancel?: () => void;
+}
+
+export const RoleForm = ({
+ groups,
+ defaultValues,
+ submitText,
+ onSubmit,
+ onCancel,
+}: RoleFormProps) => {
+ const router = useRouter();
+
+ const form = useForm({
+ resolver: zodResolver(roleFormSchema),
+ defaultValues,
+ });
+
+ const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
+ const visiblePermissionFormFields =
+ getVisiblePermissionFormFields(isCloudEnvironment);
+ const showUnlimitedVisibilityField = !!getUnlimitedVisibilityField();
+
+ const { setPermissionValue, setUnlimitedVisibility } =
+ useManageProvidersUnlimitedVisibility(form);
+ const { handleServerResponse } = useFormServerErrors(form, {
+ "/data/attributes/name": "name",
+ });
+
+ const unlimitedVisibility = !!form.watch("unlimited_visibility");
+ const isLoading = form.formState.isSubmitting;
+
+ const onSelectAllChange = (checked: boolean) => {
+ visiblePermissionFormFields.forEach(({ field }) => {
+ setPermissionValue(field, checked);
+ });
+ };
+
+ const handleCancel = onCancel ?? (() => router.push("/roles"));
+
+ return (
+
+
+ );
+};
diff --git a/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx b/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx
new file mode 100644
index 00000000000..50a2e2292d7
--- /dev/null
+++ b/ui/components/roles/workflow/forms/unlimited-visibility-section.tsx
@@ -0,0 +1,56 @@
+import { InfoIcon } from "lucide-react";
+import { ReactNode } from "react";
+
+import { Checkbox } from "@/components/shadcn/checkbox/checkbox";
+
+export const UnlimitedVisibilitySection = ({
+ children,
+}: {
+ children: ReactNode;
+}) => {
+ return (
+
+
+
+
+ This is a tenant-wide visibility setting. It grants visibility into
+ every provider, account, resource, finding, scan, and compliance
+ result, regardless of the groups selected below. It is also{" "}
+ required to use the Jira integration.
+