Browse Source

FE: Expose cluster ACL list (#3662)

Co-authored-by: iliax <ikuramshin@provectus.com>
Co-authored-by: Ilya Kuramshin <iliax@proton.me>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Nail Badiullin 2 năm trước cách đây
mục cha
commit
1d8c6197ac

+ 13 - 0
kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx

@@ -0,0 +1,13 @@
+import React from 'react';
+import { Routes, Route } from 'react-router-dom';
+import ACList from 'components/ACLPage/List/List';
+
+const ACLPage = () => {
+  return (
+    <Routes>
+      <Route index element={<ACList />} />
+    </Routes>
+  );
+};
+
+export default ACLPage;

+ 44 - 0
kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts

@@ -0,0 +1,44 @@
+import styled from 'styled-components';
+
+export const EnumCell = styled.div`
+  text-transform: capitalize;
+`;
+
+export const DeleteCell = styled.div`
+  svg {
+    cursor: pointer;
+  }
+`;
+
+export const Chip = styled.div<{
+  chipType?: 'default' | 'success' | 'danger' | 'secondary' | string;
+}>`
+  width: fit-content;
+  text-transform: capitalize;
+  padding: 2px 8px;
+  font-size: 12px;
+  line-height: 16px;
+  border-radius: 16px;
+  color: ${({ theme }) => theme.tag.color};
+  background-color: ${({ theme, chipType }) => {
+    switch (chipType) {
+      case 'success':
+        return theme.tag.backgroundColor.green;
+      case 'danger':
+        return theme.tag.backgroundColor.red;
+      case 'secondary':
+        return theme.tag.backgroundColor.secondary;
+      default:
+        return theme.tag.backgroundColor.gray;
+    }
+  }};
+`;
+
+export const PatternCell = styled.div`
+  display: flex;
+  align-items: center;
+
+  ${Chip} {
+    margin-left: 4px;
+  }
+`;

+ 153 - 0
kafka-ui-react-app/src/components/ACLPage/List/List.tsx

@@ -0,0 +1,153 @@
+import React from 'react';
+import { ColumnDef } from '@tanstack/react-table';
+import { useTheme } from 'styled-components';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import Table from 'components/common/NewTable';
+import DeleteIcon from 'components/common/Icons/DeleteIcon';
+import { useConfirm } from 'lib/hooks/useConfirm';
+import useAppParams from 'lib/hooks/useAppParams';
+import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';
+import { ClusterName } from 'redux/interfaces';
+import {
+  KafkaAcl,
+  KafkaAclNamePatternType,
+  KafkaAclPermissionEnum,
+} from 'generated-sources';
+
+import * as S from './List.styled';
+
+const ACList: React.FC = () => {
+  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
+  const theme = useTheme();
+  const { data: aclList } = useAcls(clusterName);
+  const { deleteResource } = useDeleteAcl(clusterName);
+  const modal = useConfirm(true);
+
+  const [rowId, setRowId] = React.useState('');
+
+  const onDeleteClick = (acl: KafkaAcl | null) => {
+    if (acl) {
+      modal('Are you sure want to delete this ACL record?', () =>
+        deleteResource(acl)
+      );
+    }
+  };
+
+  const columns = React.useMemo<ColumnDef<KafkaAcl>[]>(
+    () => [
+      {
+        header: 'Principal',
+        accessorKey: 'principal',
+        size: 257,
+      },
+      {
+        header: 'Resource',
+        accessorKey: 'resourceType',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>
+        ),
+        size: 145,
+      },
+      {
+        header: 'Pattern',
+        accessorKey: 'resourceName',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue, row }) => {
+          let chipType;
+          if (
+            row.original.namePatternType === KafkaAclNamePatternType.PREFIXED
+          ) {
+            chipType = 'default';
+          }
+
+          if (
+            row.original.namePatternType === KafkaAclNamePatternType.LITERAL
+          ) {
+            chipType = 'secondary';
+          }
+          return (
+            <S.PatternCell>
+              {getValue<string>()}
+              {chipType ? (
+                <S.Chip chipType={chipType}>
+                  {row.original.namePatternType.toLowerCase()}
+                </S.Chip>
+              ) : null}
+            </S.PatternCell>
+          );
+        },
+        size: 257,
+      },
+      {
+        header: 'Host',
+        accessorKey: 'host',
+        size: 257,
+      },
+      {
+        header: 'Operation',
+        accessorKey: 'operation',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>
+        ),
+        size: 121,
+      },
+      {
+        header: 'Permission',
+        accessorKey: 'permission',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.Chip
+            chipType={
+              getValue<string>() === KafkaAclPermissionEnum.ALLOW
+                ? 'success'
+                : 'danger'
+            }
+          >
+            {getValue<string>().toLowerCase()}
+          </S.Chip>
+        ),
+        size: 111,
+      },
+      {
+        id: 'delete',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ row }) => {
+          return (
+            <S.DeleteCell onClick={() => onDeleteClick(row.original)}>
+              <DeleteIcon
+                fill={
+                  rowId === row.id ? theme.acl.table.deleteIcon : 'transparent'
+                }
+              />
+            </S.DeleteCell>
+          );
+        },
+        size: 76,
+      },
+    ],
+    [rowId]
+  );
+
+  const onRowHover = (value: unknown) => {
+    if (value && typeof value === 'object' && 'id' in value) {
+      setRowId(value.id as string);
+    }
+  };
+
+  return (
+    <>
+      <PageHeading text="Access Control List" />
+      <Table
+        columns={columns}
+        data={aclList ?? []}
+        emptyMessage="No ACL items found"
+        onRowHover={onRowHover}
+        onMouseLeave={() => setRowId('')}
+      />
+    </>
+  );
+};
+
+export default ACList;

+ 74 - 0
kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx

@@ -0,0 +1,74 @@
+import React from 'react';
+import { render, WithRoute } from 'lib/testHelpers';
+import { screen } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import { clusterACLPath } from 'lib/paths';
+import ACList from 'components/ACLPage/List/List';
+import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';
+import { aclPayload } from 'lib/fixtures/acls';
+
+jest.mock('lib/hooks/api/acl', () => ({
+  useAcls: jest.fn(),
+  useDeleteAcl: jest.fn(),
+}));
+
+describe('ACLList Component', () => {
+  const clusterName = 'local';
+  const renderComponent = () =>
+    render(
+      <WithRoute path={clusterACLPath()}>
+        <ACList />
+      </WithRoute>,
+      {
+        initialEntries: [clusterACLPath(clusterName)],
+      }
+    );
+
+  describe('ACLList', () => {
+    describe('when the acls are loaded', () => {
+      beforeEach(() => {
+        (useAcls as jest.Mock).mockImplementation(() => ({
+          data: aclPayload,
+        }));
+        (useDeleteAcl as jest.Mock).mockImplementation(() => ({
+          deleteResource: jest.fn(),
+        }));
+      });
+
+      it('renders ACLList with records', async () => {
+        renderComponent();
+        expect(screen.getByRole('table')).toBeInTheDocument();
+        expect(screen.getAllByRole('row').length).toEqual(4);
+      });
+
+      it('shows delete icon on hover', async () => {
+        const { container } = renderComponent();
+        const [trElement] = screen.getAllByRole('row');
+        await userEvent.hover(trElement);
+        const deleteElement = container.querySelector('svg');
+        expect(deleteElement).not.toHaveStyle({
+          fill: 'transparent',
+        });
+      });
+    });
+
+    describe('when it has no acls', () => {
+      beforeEach(() => {
+        (useAcls as jest.Mock).mockImplementation(() => ({
+          data: [],
+        }));
+        (useDeleteAcl as jest.Mock).mockImplementation(() => ({
+          deleteResource: jest.fn(),
+        }));
+      });
+
+      it('renders empty ACLList with message', async () => {
+        renderComponent();
+        expect(screen.getByRole('table')).toBeInTheDocument();
+        expect(
+          screen.getByRole('row', { name: 'No ACL items found' })
+        ).toBeInTheDocument();
+      });
+    });
+  });
+});

+ 11 - 0
kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx

@@ -13,6 +13,7 @@ import {
   clusterTopicsRelativePath,
   clusterConfigRelativePath,
   getNonExactPath,
+  clusterAclRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -30,6 +31,7 @@ const ClusterConfigPage = React.lazy(
 const ConsumerGroups = React.lazy(
   () => import('components/ConsumerGroups/ConsumerGroups')
 );
+const AclPage = React.lazy(() => import('components/ACLPage/ACLPage'));
 
 const ClusterPage: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
@@ -51,6 +53,9 @@ const ClusterPage: React.FC = () => {
         ClusterFeaturesEnum.TOPIC_DELETION
       ),
       hasKsqlDbConfigured: features.includes(ClusterFeaturesEnum.KSQL_DB),
+      hasAclViewConfigured:
+        features.includes(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||
+        features.includes(ClusterFeaturesEnum.KAFKA_ACL_EDIT),
     };
   }, [clusterName, data]);
 
@@ -95,6 +100,12 @@ const ClusterPage: React.FC = () => {
                 element={<KsqlDb />}
               />
             )}
+            {contextValue.hasAclViewConfigured && (
+              <Route
+                path={getNonExactPath(clusterAclRelativePath)}
+                element={<AclPage />}
+              />
+            )}
             {appInfo.hasDynamicConfig && (
               <Route
                 path={getNonExactPath(clusterConfigRelativePath)}

+ 5 - 0
kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx

@@ -7,6 +7,7 @@ import {
   clusterSchemasPath,
   clusterConnectorsPath,
   clusterKsqlDbPath,
+  clusterACLPath,
 } from 'lib/paths';
 
 import ClusterMenuItem from './ClusterMenuItem';
@@ -57,6 +58,10 @@ const ClusterMenu: React.FC<Props> = ({
           {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (
             <ClusterMenuItem to={clusterKsqlDbPath(name)} title="KSQL DB" />
           )}
+          {(hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||
+            hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_EDIT)) && (
+            <ClusterMenuItem to={clusterACLPath(name)} title="ACL" />
+          )}
         </S.List>
       )}
     </S.List>

+ 1 - 1
kafka-ui-react-app/src/components/common/Button/Button.styled.ts

@@ -1,7 +1,7 @@
 import styled from 'styled-components';
 
 export interface ButtonProps {
-  buttonType: 'primary' | 'secondary';
+  buttonType: 'primary' | 'secondary' | 'danger';
   buttonSize: 'S' | 'M' | 'L';
   isInverted?: boolean;
 }

+ 1 - 1
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx

@@ -26,7 +26,7 @@ const ConfirmationModal: React.FC = () => {
             Cancel
           </Button>
           <Button
-            buttonType="primary"
+            buttonType={context.dangerButton ? 'danger' : 'primary'}
             buttonSize="M"
             onClick={context.confirm}
             type="button"

+ 3 - 2
kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import { useTheme } from 'styled-components';
 
-const DeleteIcon: React.FC = () => {
+const DeleteIcon: React.FC<{ fill?: string }> = ({ fill }) => {
   const theme = useTheme();
+  const curentFill = fill || theme.editFilter.deleteIconColor;
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
       viewBox="0 0 448 512"
-      fill={theme.editFilter.deleteIconColor}
+      fill={curentFill}
       width="14"
       height="14"
     >

+ 8 - 4
kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts

@@ -1,9 +1,12 @@
 import styled from 'styled-components';
 import { MultiSelect as ReactMultiSelect } from 'react-multi-select-component';
 
-const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>`
+const MultiSelect = styled(ReactMultiSelect)<{
+  minWidth?: string;
+  height?: string;
+}>`
   min-width: ${({ minWidth }) => minWidth || '200px;'};
-  height: 32px;
+  height: ${({ height }) => height ?? '32px'};
   font-size: 14px;
   .search input {
     color: ${({ theme }) => theme.input.color.normal};
@@ -36,13 +39,14 @@ const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>`
     &:hover {
       border-color: ${({ theme }) => theme.select.borderColor.hover} !important;
     }
-    height: 32px;
+
+    height: ${({ height }) => height ?? '32px'};
     * {
       cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
     }
 
     & > .dropdown-heading {
-      height: 32px;
+      height: ${({ height }) => height ?? '32px'};
       color: ${({ disabled, theme }) =>
         disabled
           ? theme.select.color.disabled

+ 35 - 1
kafka-ui-react-app/src/components/common/NewTable/Table.tsx

@@ -52,6 +52,9 @@ export interface TableProps<TData> {
 
   // Handles row click. Can not be combined with `enableRowSelection` && expandable rows.
   onRowClick?: (row: Row<TData>) => void;
+
+  onRowHover?: (row: Row<TData>) => void;
+  onMouseLeave?: () => void;
 }
 
 type UpdaterFn<T> = (previousState: T) => T;
@@ -127,6 +130,8 @@ const Table: React.FC<TableProps<any>> = ({
   emptyMessage,
   disabled,
   onRowClick,
+  onRowHover,
+  onMouseLeave,
 }) => {
   const [searchParams, setSearchParams] = useSearchParams();
   const location = useLocation();
@@ -194,6 +199,21 @@ const Table: React.FC<TableProps<any>> = ({
     return undefined;
   };
 
+  const handleRowHover = (row: Row<typeof data>) => (e: React.MouseEvent) => {
+    if (onRowHover) {
+      e.stopPropagation();
+      return onRowHover(row);
+    }
+
+    return undefined;
+  };
+
+  const handleMouseLeave = () => {
+    if (onMouseLeave) {
+      onMouseLeave();
+    }
+  };
+
   return (
     <>
       {BatchActionsBar && (
@@ -227,6 +247,12 @@ const Table: React.FC<TableProps<any>> = ({
                     sortable={header.column.getCanSort()}
                     sortOrder={header.column.getIsSorted()}
                     onClick={header.column.getToggleSortingHandler()}
+                    style={{
+                      width:
+                        header.column.getSize() !== 150
+                          ? header.column.getSize()
+                          : undefined,
+                    }}
                   >
                     <div>
                       {flexRender(
@@ -245,6 +271,8 @@ const Table: React.FC<TableProps<any>> = ({
                 <S.Row
                   expanded={row.getIsExpanded()}
                   onClick={handleRowClick(row)}
+                  onMouseOver={onRowHover ? handleRowHover(row) : undefined}
+                  onMouseLeave={onMouseLeave ? handleMouseLeave : undefined}
                   clickable={
                     !enableRowSelection &&
                     (row.getCanExpand() || onRowClick !== undefined)
@@ -269,7 +297,13 @@ const Table: React.FC<TableProps<any>> = ({
                   {row
                     .getVisibleCells()
                     .map(({ id, getContext, column: { columnDef } }) => (
-                      <td key={id} style={columnDef.meta}>
+                      <td
+                        key={id}
+                        style={{
+                          width:
+                            columnDef.size !== 150 ? columnDef.size : undefined,
+                        }}
+                      >
                         {flexRender(columnDef.cell, getContext())}
                       </td>
                     ))}

+ 5 - 0
kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx

@@ -6,6 +6,8 @@ interface ConfirmContextType {
   setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;
   setConfirm: React.Dispatch<React.SetStateAction<(() => void) | undefined>>;
   cancel: () => void;
+  dangerButton: boolean;
+  setDangerButton: React.Dispatch<React.SetStateAction<boolean>>;
 }
 
 export const ConfirmContext = React.createContext<ConfirmContextType | null>(
@@ -17,6 +19,7 @@ export const ConfirmContextProvider: React.FC<
 > = ({ children }) => {
   const [content, setContent] = useState<React.ReactNode>(null);
   const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined);
+  const [dangerButton, setDangerButton] = useState(false);
 
   const cancel = () => {
     setContent(null);
@@ -31,6 +34,8 @@ export const ConfirmContextProvider: React.FC<
         confirm,
         setConfirm,
         cancel,
+        dangerButton,
+        setDangerButton,
       }}
     >
       {children}

+ 2 - 0
kafka-ui-react-app/src/lib/api.ts

@@ -10,6 +10,7 @@ import {
   ConsumerGroupsApi,
   AuthorizationApi,
   ApplicationConfigApi,
+  AclsApi,
 } from 'generated-sources';
 import { BASE_PARAMS } from 'lib/constants';
 
@@ -25,3 +26,4 @@ export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf);
 export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
 export const authApiClient = new AuthorizationApi(apiClientConf);
 export const appConfigApiClient = new ApplicationConfigApi(apiClientConf);
+export const aclApiClient = new AclsApi(apiClientConf);

+ 37 - 0
kafka-ui-react-app/src/lib/fixtures/acls.ts

@@ -0,0 +1,37 @@
+import {
+  KafkaAcl,
+  KafkaAclResourceType,
+  KafkaAclNamePatternType,
+  KafkaAclPermissionEnum,
+  KafkaAclOperationEnum,
+} from 'generated-sources';
+
+export const aclPayload: KafkaAcl[] = [
+  {
+    principal: 'User 1',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.LITERAL,
+    permission: KafkaAclPermissionEnum.ALLOW,
+    operation: KafkaAclOperationEnum.READ,
+  },
+  {
+    principal: 'User 2',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.PREFIXED,
+    permission: KafkaAclPermissionEnum.ALLOW,
+    operation: KafkaAclOperationEnum.READ,
+  },
+  {
+    principal: 'User 3',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.LITERAL,
+    permission: KafkaAclPermissionEnum.DENY,
+    operation: KafkaAclOperationEnum.READ,
+  },
+];

+ 67 - 0
kafka-ui-react-app/src/lib/hooks/api/acl.ts

@@ -0,0 +1,67 @@
+import { aclApiClient as api } from 'lib/api';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { ClusterName } from 'redux/interfaces';
+import { showSuccessAlert } from 'lib/errorHandling';
+import { KafkaAcl } from 'generated-sources';
+
+export function useAcls(clusterName: ClusterName) {
+  return useQuery(
+    ['clusters', clusterName, 'acls'],
+    () => api.listAcls({ clusterName }),
+    {
+      suspense: false,
+    }
+  );
+}
+
+export function useCreateAclMutation(clusterName: ClusterName) {
+  return useMutation(
+    (data: KafkaAcl) =>
+      api.createAcl({
+        clusterName,
+        kafkaAcl: data,
+      }),
+    {
+      onSuccess() {
+        showSuccessAlert({
+          message: 'Your ACL was created successfully',
+        });
+      },
+    }
+  );
+}
+
+export function useCreateAcl(clusterName: ClusterName) {
+  const mutate = useCreateAclMutation(clusterName);
+
+  return {
+    createResource: async (param: KafkaAcl) => {
+      return mutate.mutateAsync(param);
+    },
+    ...mutate,
+  };
+}
+
+export function useDeleteAclMutation(clusterName: ClusterName) {
+  const queryClient = useQueryClient();
+  return useMutation(
+    (acl: KafkaAcl) => api.deleteAcl({ clusterName, kafkaAcl: acl }),
+    {
+      onSuccess: () => {
+        showSuccessAlert({ message: 'ACL deleted' });
+        queryClient.invalidateQueries(['clusters', clusterName, 'acls']);
+      },
+    }
+  );
+}
+
+export function useDeleteAcl(clusterName: ClusterName) {
+  const mutate = useDeleteAclMutation(clusterName);
+
+  return {
+    deleteResource: async (param: KafkaAcl) => {
+      return mutate.mutateAsync(param);
+    },
+    ...mutate,
+  };
+}

+ 2 - 1
kafka-ui-react-app/src/lib/hooks/useConfirm.ts

@@ -1,12 +1,13 @@
 import { ConfirmContext } from 'components/contexts/ConfirmContext';
 import React, { useContext } from 'react';
 
-export const useConfirm = () => {
+export const useConfirm = (danger = false) => {
   const context = useContext(ConfirmContext);
   return (
     message: React.ReactNode,
     callback: () => void | Promise<unknown>
   ) => {
+    context?.setDangerButton(danger);
     context?.setContent(message);
     context?.setConfirm(() => async () => {
       await callback();

+ 7 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -285,3 +285,10 @@ export const clusterConfigPath = (
 
 const clusterNewConfigRelativePath = 'create-new-cluster';
 export const clusterNewConfigPath = `/ui/clusters/${clusterNewConfigRelativePath}`;
+
+// ACL
+export const clusterAclRelativePath = 'acl';
+export const clusterAclNewRelativePath = 'create-new-acl';
+export const clusterACLPath = (
+  clusterName: ClusterName = RouteParams.clusterName
+) => `${clusterPath(clusterName)}/${clusterAclRelativePath}`;

+ 82 - 4
kafka-ui-react-app/src/theme/theme.ts

@@ -31,6 +31,7 @@ const Colors = {
     '15': '#C2F0D1',
     '30': '#85E0A3',
     '40': '#5CD685',
+    '50': '#33CC66',
     '60': '#29A352',
   },
   brand: {
@@ -231,6 +232,7 @@ const baseTheme = {
       white: Colors.neutral[10],
       red: Colors.red[10],
       blue: Colors.blue[10],
+      secondary: Colors.neutral[15],
     },
     color: Colors.neutral[90],
   },
@@ -416,8 +418,8 @@ export const theme = {
         disabled: Colors.red[20],
       },
       color: {
-        normal: Colors.neutral[90],
-        disabled: Colors.neutral[30],
+        normal: Colors.neutral[0],
+        disabled: Colors.neutral[0],
       },
       invertedColors: {
         normal: Colors.brand[50],
@@ -695,6 +697,44 @@ export const theme = {
     textColor: Colors.brand[50],
     deleteIconColor: Colors.brand[50],
   },
+  acl: {
+    table: {
+      deleteIcon: Colors.neutral[50],
+    },
+    create: {
+      radioButtons: {
+        green: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.green[50],
+            text: Colors.neutral[0],
+          },
+          hover: {
+            background: Colors.green[10],
+            text: Colors.neutral[90],
+          },
+        },
+        gray: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.neutral[10],
+            text: Colors.neutral[90],
+          },
+          hover: {
+            background: Colors.neutral[5],
+            text: Colors.neutral[90],
+          },
+        },
+        red: {},
+      },
+    },
+  },
 };
 
 export type ThemeType = typeof theme;
@@ -818,8 +858,8 @@ export const darkTheme: ThemeType = {
         disabled: Colors.red[20],
       },
       color: {
-        normal: Colors.neutral[90],
-        disabled: Colors.neutral[30],
+        normal: Colors.neutral[0],
+        disabled: Colors.neutral[0],
       },
       invertedColors: {
         normal: Colors.brand[50],
@@ -1155,4 +1195,42 @@ export const darkTheme: ThemeType = {
       color: Colors.neutral[0],
     },
   },
+  acl: {
+    table: {
+      deleteIcon: Colors.neutral[50],
+    },
+    create: {
+      radioButtons: {
+        green: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.green[50],
+            text: Colors.neutral[0],
+          },
+          hover: {
+            background: Colors.green[10],
+            text: Colors.neutral[0],
+          },
+        },
+        gray: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.neutral[10],
+            text: Colors.neutral[90],
+          },
+          hover: {
+            background: Colors.neutral[5],
+            text: Colors.neutral[90],
+          },
+        },
+        red: {},
+      },
+    },
+  },
 };