mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Enterprise Search] Refactor RoleMappingsTable to use EuiInMemoryTable (#101918)
* Add shared actions component Both tables use the same actions * Refactor RoleMappingsTable to use EuiInMemoryTable This is way better than the bespoke one I wrote and it comes with pagination for free - Also fixes a typo in the i18n id
This commit is contained in:
parent
61677f7a77
commit
797c0c90b0
8 changed files with 198 additions and 156 deletions
|
@ -200,3 +200,8 @@ export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate(
|
|||
'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingButton',
|
||||
{ defaultMessage: 'Create a new role mapping' }
|
||||
);
|
||||
|
||||
export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.roleMapping.noResults.message',
|
||||
{ defaultMessage: 'Create a new role mapping' }
|
||||
);
|
||||
|
|
|
@ -11,3 +11,4 @@ export { RoleOptionLabel } from './role_option_label';
|
|||
export { RoleSelector } from './role_selector';
|
||||
export { RoleMappingFlyout } from './role_mapping_flyout';
|
||||
export { RoleMappingsHeading } from './role_mappings_heading';
|
||||
export { UsersAndRolesRowActions } from './users_and_roles_row_actions';
|
||||
|
|
|
@ -9,13 +9,14 @@ import { wsRoleMapping, asRoleMapping } from './__mocks__/roles';
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { EuiFieldSearch, EuiTableRow } from '@elastic/eui';
|
||||
import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui';
|
||||
|
||||
import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
|
||||
|
||||
import { RoleMappingsTable } from './role_mappings_table';
|
||||
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
|
||||
|
||||
describe('RoleMappingsTable', () => {
|
||||
const initializeRoleMapping = jest.fn();
|
||||
|
@ -41,55 +42,44 @@ describe('RoleMappingsTable', () => {
|
|||
handleDeleteMapping,
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
it('renders with "shouldShowAuthProvider" true', () => {
|
||||
const wrapper = mount(<RoleMappingsTable {...props} />);
|
||||
|
||||
expect(wrapper.find(EuiFieldSearch)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTableRow)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('renders with "shouldShowAuthProvider" false', () => {
|
||||
const wrapper = mount(<RoleMappingsTable {...props} shouldShowAuthProvider={false} />);
|
||||
|
||||
expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('renders auth provider display names', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
const wrapper = mount(<RoleMappingsTable {...props} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="AuthProviderDisplay"]').prop('children')).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual(
|
||||
`${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles input change', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
const input = wrapper.find(EuiFieldSearch);
|
||||
const value = 'Query';
|
||||
input.simulate('change', { target: { value } });
|
||||
|
||||
expect(wrapper.find(EuiTableRow)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles manage click', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
wrapper.find('[data-test-subj="ManageButton"]').simulate('click');
|
||||
const wrapper = mount(<RoleMappingsTable {...props} />);
|
||||
wrapper.find(UsersAndRolesRowActions).prop('onManageClick')();
|
||||
|
||||
expect(initializeRoleMapping).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete click', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
wrapper.find('[data-test-subj="DeleteButton"]').simulate('click');
|
||||
const wrapper = mount(<RoleMappingsTable {...props} />);
|
||||
wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')();
|
||||
|
||||
expect(handleDeleteMapping).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change with special chars', () => {
|
||||
const wrapper = shallow(<RoleMappingsTable {...props} />);
|
||||
const input = wrapper.find(EuiFieldSearch);
|
||||
const value = '*//username';
|
||||
input.simulate('change', { target: { value } });
|
||||
|
||||
expect(wrapper.find(EuiTableRow)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows default message when "accessAllEngines" is true', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = mount(
|
||||
<RoleMappingsTable {...props} roleMappings={[asRoleMapping as any]} accessItemKey="engines" />
|
||||
);
|
||||
|
||||
|
@ -100,7 +90,7 @@ describe('RoleMappingsTable', () => {
|
|||
const noItemsRoleMapping = { ...asRoleMapping, engines: [] };
|
||||
noItemsRoleMapping.accessAllEngines = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
const wrapper = mount(
|
||||
<RoleMappingsTable
|
||||
{...props}
|
||||
roleMappings={[noItemsRoleMapping as any]}
|
||||
|
|
|
@ -5,26 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFieldSearch,
|
||||
EuiIconTip,
|
||||
EuiSpacer,
|
||||
EuiTable,
|
||||
EuiTableBody,
|
||||
EuiTableHeader,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
|
||||
|
||||
import { ASRoleMapping } from '../../app_search/types';
|
||||
import { WSRoleMapping } from '../../workplace_search/types';
|
||||
import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
|
||||
import { RoleRules } from '../types';
|
||||
|
||||
import './role_mappings_table.scss';
|
||||
|
@ -38,7 +24,9 @@ import {
|
|||
EXTERNAL_ATTRIBUTE_LABEL,
|
||||
ATTRIBUTE_VALUE_LABEL,
|
||||
FILTER_ROLE_MAPPINGS_PLACEHOLDER,
|
||||
ROLE_MAPPINGS_NO_RESULTS_MESSAGE,
|
||||
} from './constants';
|
||||
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
|
||||
|
||||
interface AccessItem {
|
||||
name: string;
|
||||
|
@ -58,8 +46,6 @@ interface Props {
|
|||
handleDeleteMapping(roleMappingId: string): void;
|
||||
}
|
||||
|
||||
const MAX_CELL_WIDTH = 24;
|
||||
|
||||
const noItemsPlaceholder = <EuiTextColor color="subdued">—</EuiTextColor>;
|
||||
|
||||
const getAuthProviderDisplayValue = (authProvider: string) =>
|
||||
|
@ -73,114 +59,104 @@ export const RoleMappingsTable: React.FC<Props> = ({
|
|||
initializeRoleMapping,
|
||||
handleDeleteMapping,
|
||||
}) => {
|
||||
const [filterValue, updateValue] = useState('');
|
||||
|
||||
// This is needed because App Search has `engines` and Workplace Search has `groups`.
|
||||
const standardizeRoleMapping = (roleMappings as SharedRoleMapping[]).map((rm) => {
|
||||
const _rm = { ...rm } as SharedRoleMapping;
|
||||
_rm.accessItems = rm[accessItemKey];
|
||||
return _rm;
|
||||
});
|
||||
|
||||
const filterResults = (result: SharedRoleMapping) => {
|
||||
// Filter out non-alphanumeric characters, except for underscores, hyphens, and spaces
|
||||
const sanitizedValue = filterValue.replace(/[^\w\s-]/g, '');
|
||||
const values = Object.values(result);
|
||||
const regexp = new RegExp(sanitizedValue, 'i');
|
||||
return values.filter((x) => regexp.test(x)).length > 0;
|
||||
};
|
||||
|
||||
const filteredResults = standardizeRoleMapping.filter(filterResults);
|
||||
const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0];
|
||||
const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1];
|
||||
|
||||
const rowActions = (id: string) => (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
onClick={() => initializeRoleMapping(id)}
|
||||
iconType="pencil"
|
||||
aria-label={MANAGE_BUTTON_LABEL}
|
||||
data-test-subj="ManageButton"
|
||||
/>{' '}
|
||||
<EuiButtonIcon
|
||||
onClick={() => handleDeleteMapping(id)}
|
||||
iconType="trash"
|
||||
aria-label={DELETE_BUTTON_LABEL}
|
||||
data-test-subj="DeleteButton"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
// This is needed because App Search has `engines` and Workplace Search has `groups`.
|
||||
const standardizedRoleMappings = (roleMappings as SharedRoleMapping[]).map((rm) => {
|
||||
const _rm = { ...rm } as SharedRoleMapping;
|
||||
_rm.accessItems = rm[accessItemKey];
|
||||
return _rm;
|
||||
}) as SharedRoleMapping[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFieldSearch
|
||||
value={filterValue}
|
||||
placeholder={FILTER_ROLE_MAPPINGS_PLACEHOLDER}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
const attributeNameCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'attribute',
|
||||
name: EXTERNAL_ATTRIBUTE_LABEL,
|
||||
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules),
|
||||
};
|
||||
|
||||
const attributeValueCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'attributeValue',
|
||||
name: ATTRIBUTE_VALUE_LABEL,
|
||||
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
|
||||
};
|
||||
|
||||
const roleCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'roleType',
|
||||
name: ROLE_LABEL,
|
||||
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
|
||||
};
|
||||
|
||||
const accessItemsCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'accessItems',
|
||||
name: accessHeader,
|
||||
render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => (
|
||||
<span data-test-subj="AccessItemsList">
|
||||
{accessAllEngines ? (
|
||||
ALL_LABEL
|
||||
) : (
|
||||
<>
|
||||
{accessItems.length === 0
|
||||
? noItemsPlaceholder
|
||||
: accessItems.map(({ name }) => (
|
||||
<Fragment key={name}>
|
||||
{name}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
const authProviderCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'authProvider',
|
||||
name: AUTH_PROVIDER_LABEL,
|
||||
render: (_, { authProvider }: SharedRoleMapping) => (
|
||||
<span data-test-subj="AuthProviderDisplayValue">
|
||||
{authProvider.map(getAuthProviderDisplayValue).join(', ')}
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
const actionsCol: EuiBasicTableColumn<SharedRoleMapping> = {
|
||||
field: 'id',
|
||||
name: '',
|
||||
align: 'right',
|
||||
render: (_, { id }: SharedRoleMapping) => (
|
||||
<UsersAndRolesRowActions
|
||||
onManageClick={() => initializeRoleMapping(id)}
|
||||
onDeleteClick={() => handleDeleteMapping(id)}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{filteredResults.length > 0 ? (
|
||||
<EuiTable className="roleMappingsTable">
|
||||
<EuiTableHeader>
|
||||
<EuiTableHeaderCell>{EXTERNAL_ATTRIBUTE_LABEL}</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell>{ATTRIBUTE_VALUE_LABEL}</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell>{ROLE_LABEL}</EuiTableHeaderCell>
|
||||
<EuiTableHeaderCell>{accessHeader}</EuiTableHeaderCell>
|
||||
{shouldShowAuthProvider && (
|
||||
<EuiTableHeaderCell>{AUTH_PROVIDER_LABEL}</EuiTableHeaderCell>
|
||||
)}
|
||||
<EuiTableHeaderCell />
|
||||
</EuiTableHeader>
|
||||
<EuiTableBody>
|
||||
{filteredResults.map(
|
||||
({ id, authProvider, rules, roleType, accessAllEngines, accessItems, toolTip }) => (
|
||||
<EuiTableRow key={id}>
|
||||
<EuiTableRowCell>{getFirstAttributeName(rules)}</EuiTableRowCell>
|
||||
<EuiTableRowCell style={{ maxWidth: MAX_CELL_WIDTH }}>
|
||||
{getFirstAttributeValue(rules)}
|
||||
</EuiTableRowCell>
|
||||
<EuiTableRowCell>{roleType}</EuiTableRowCell>
|
||||
<EuiTableRowCell
|
||||
data-test-subj="AccessItemsList"
|
||||
style={{ maxWidth: MAX_CELL_WIDTH }}
|
||||
>
|
||||
{accessAllEngines ? (
|
||||
ALL_LABEL
|
||||
) : (
|
||||
<>
|
||||
{accessItems.length === 0
|
||||
? noItemsPlaceholder
|
||||
: accessItems.map(({ name }) => (
|
||||
<Fragment key={name}>
|
||||
{name}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</EuiTableRowCell>
|
||||
{shouldShowAuthProvider && (
|
||||
<EuiTableRowCell data-test-subj="AuthProviderDisplay">
|
||||
{authProvider.map(getAuthProviderDisplayValue).join(', ')}
|
||||
</EuiTableRowCell>
|
||||
)}
|
||||
<EuiTableRowCell align="right">
|
||||
{id && rowActions(id)}
|
||||
{toolTip && <EuiIconTip position="left" content={toolTip.content} />}
|
||||
</EuiTableRowCell>
|
||||
</EuiTableRow>
|
||||
)
|
||||
)}
|
||||
</EuiTableBody>
|
||||
</EuiTable>
|
||||
) : (
|
||||
<p>
|
||||
{i18n.translate('xpack.enterpriseSearch.roleMapping.moResults.message', {
|
||||
defaultMessage: "No results found for '{filterValue}'",
|
||||
values: { filterValue },
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
const columns = shouldShowAuthProvider
|
||||
? [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, authProviderCol, actionsCol]
|
||||
: [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, actionsCol];
|
||||
|
||||
const pagination = {
|
||||
hidePerPageOptions: true,
|
||||
};
|
||||
|
||||
const search = {
|
||||
box: {
|
||||
incremental: true,
|
||||
fullWidth: false,
|
||||
placeholder: FILTER_ROLE_MAPPINGS_PLACEHOLDER,
|
||||
'data-test-subj': 'RoleMappingsTableSearchInput',
|
||||
},
|
||||
};
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
data-test-subj="RoleMappingsTable"
|
||||
columns={columns}
|
||||
items={standardizedRoleMappings}
|
||||
search={search}
|
||||
pagination={pagination}
|
||||
message={ROLE_MAPPINGS_NO_RESULTS_MESSAGE}
|
||||
responsive={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
|
||||
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
|
||||
|
||||
describe('UsersAndRolesRowActions', () => {
|
||||
const onManageClick = jest.fn();
|
||||
const onDeleteClick = jest.fn();
|
||||
|
||||
const props = {
|
||||
onManageClick,
|
||||
onDeleteClick,
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<UsersAndRolesRowActions {...props} />);
|
||||
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('handles manage click', () => {
|
||||
const wrapper = shallow(<UsersAndRolesRowActions {...props} />);
|
||||
const button = wrapper.find(EuiButtonIcon).first();
|
||||
button.simulate('click');
|
||||
|
||||
expect(onManageClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete click', () => {
|
||||
const wrapper = shallow(<UsersAndRolesRowActions {...props} />);
|
||||
const button = wrapper.find(EuiButtonIcon).last();
|
||||
button.simulate('click');
|
||||
|
||||
expect(onDeleteClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
|
||||
import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
|
||||
|
||||
interface Props {
|
||||
onManageClick(): void;
|
||||
onDeleteClick(): void;
|
||||
}
|
||||
|
||||
export const UsersAndRolesRowActions: React.FC<Props> = ({ onManageClick, onDeleteClick }) => (
|
||||
<>
|
||||
<EuiButtonIcon onClick={onManageClick} iconType="pencil" aria-label={MANAGE_BUTTON_LABEL} />{' '}
|
||||
<EuiButtonIcon onClick={onDeleteClick} iconType="trash" aria-label={DELETE_BUTTON_LABEL} />
|
||||
</>
|
||||
);
|
|
@ -7966,7 +7966,7 @@
|
|||
"xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "ロールをフィルタリング...",
|
||||
"xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "個別の認証プロバイダーを選択",
|
||||
"xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "ロールマッピングを管理",
|
||||
"xpack.enterpriseSearch.roleMapping.moResults.message": "'{filterValue}'の結果が見つかりません。",
|
||||
"xpack.enterpriseSearch.roleMapping.noResults.message": "の結果が見つかりません。",
|
||||
"xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "ロールマッピングを追加",
|
||||
"xpack.enterpriseSearch.roleMapping.roleLabel": "ロール",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール",
|
||||
|
|
|
@ -8034,7 +8034,7 @@
|
|||
"xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "筛选角色......",
|
||||
"xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "选择单个身份验证提供程序",
|
||||
"xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "管理角色映射",
|
||||
"xpack.enterpriseSearch.roleMapping.moResults.message": "找不到“{filterValue}”的结果",
|
||||
"xpack.enterpriseSearch.roleMapping.noResults.message": "找不到的结果",
|
||||
"xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "添加角色映射",
|
||||
"xpack.enterpriseSearch.roleMapping.roleLabel": "角色",
|
||||
"xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue