Skip to content

Commit 797c0c9

Browse files
[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
1 parent 61677f7 commit 797c0c9

8 files changed

Lines changed: 192 additions & 150 deletions

File tree

x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,8 @@ export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate(
200200
'xpack.enterpriseSearch.roleMapping.roleMappingsHeadingButton',
201201
{ defaultMessage: 'Create a new role mapping' }
202202
);
203+
204+
export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate(
205+
'xpack.enterpriseSearch.roleMapping.noResults.message',
206+
{ defaultMessage: 'Create a new role mapping' }
207+
);

x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export { RoleOptionLabel } from './role_option_label';
1111
export { RoleSelector } from './role_selector';
1212
export { RoleMappingFlyout } from './role_mapping_flyout';
1313
export { RoleMappingsHeading } from './role_mappings_heading';
14+
export { UsersAndRolesRowActions } from './users_and_roles_row_actions';

x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { wsRoleMapping, asRoleMapping } from './__mocks__/roles';
99

1010
import React from 'react';
1111

12-
import { shallow } from 'enzyme';
12+
import { mount } from 'enzyme';
1313

14-
import { EuiFieldSearch, EuiTableRow } from '@elastic/eui';
14+
import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui';
1515

1616
import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants';
1717

1818
import { RoleMappingsTable } from './role_mappings_table';
19+
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
1920

2021
describe('RoleMappingsTable', () => {
2122
const initializeRoleMapping = jest.fn();
@@ -41,55 +42,44 @@ describe('RoleMappingsTable', () => {
4142
handleDeleteMapping,
4243
};
4344

44-
it('renders', () => {
45-
const wrapper = shallow(<RoleMappingsTable {...props} />);
45+
it('renders with "shouldShowAuthProvider" true', () => {
46+
const wrapper = mount(<RoleMappingsTable {...props} />);
4647

47-
expect(wrapper.find(EuiFieldSearch)).toHaveLength(1);
48-
expect(wrapper.find(EuiTableRow)).toHaveLength(1);
48+
expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
49+
expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(6);
4950
});
5051

51-
it('renders auth provider display names', () => {
52-
const wrapper = shallow(<RoleMappingsTable {...props} />);
52+
it('renders with "shouldShowAuthProvider" false', () => {
53+
const wrapper = mount(<RoleMappingsTable {...props} shouldShowAuthProvider={false} />);
5354

54-
expect(wrapper.find('[data-test-subj="AuthProviderDisplay"]').prop('children')).toEqual(
55-
`${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth`
56-
);
55+
expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
56+
expect(wrapper.find(EuiTableHeaderCell)).toHaveLength(5);
5757
});
5858

59-
it('handles input change', () => {
60-
const wrapper = shallow(<RoleMappingsTable {...props} />);
61-
const input = wrapper.find(EuiFieldSearch);
62-
const value = 'Query';
63-
input.simulate('change', { target: { value } });
59+
it('renders auth provider display names', () => {
60+
const wrapper = mount(<RoleMappingsTable {...props} />);
6461

65-
expect(wrapper.find(EuiTableRow)).toHaveLength(0);
62+
expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual(
63+
`${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth`
64+
);
6665
});
6766

6867
it('handles manage click', () => {
69-
const wrapper = shallow(<RoleMappingsTable {...props} />);
70-
wrapper.find('[data-test-subj="ManageButton"]').simulate('click');
68+
const wrapper = mount(<RoleMappingsTable {...props} />);
69+
wrapper.find(UsersAndRolesRowActions).prop('onManageClick')();
7170

7271
expect(initializeRoleMapping).toHaveBeenCalled();
7372
});
7473

7574
it('handles delete click', () => {
76-
const wrapper = shallow(<RoleMappingsTable {...props} />);
77-
wrapper.find('[data-test-subj="DeleteButton"]').simulate('click');
75+
const wrapper = mount(<RoleMappingsTable {...props} />);
76+
wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')();
7877

7978
expect(handleDeleteMapping).toHaveBeenCalled();
8079
});
8180

82-
it('handles input change with special chars', () => {
83-
const wrapper = shallow(<RoleMappingsTable {...props} />);
84-
const input = wrapper.find(EuiFieldSearch);
85-
const value = '*//username';
86-
input.simulate('change', { target: { value } });
87-
88-
expect(wrapper.find(EuiTableRow)).toHaveLength(1);
89-
});
90-
9181
it('shows default message when "accessAllEngines" is true', () => {
92-
const wrapper = shallow(
82+
const wrapper = mount(
9383
<RoleMappingsTable {...props} roleMappings={[asRoleMapping as any]} accessItemKey="engines" />
9484
);
9585

@@ -100,7 +90,7 @@ describe('RoleMappingsTable', () => {
10090
const noItemsRoleMapping = { ...asRoleMapping, engines: [] };
10191
noItemsRoleMapping.accessAllEngines = false;
10292

103-
const wrapper = shallow(
93+
const wrapper = mount(
10494
<RoleMappingsTable
10595
{...props}
10696
roleMappings={[noItemsRoleMapping as any]}

x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx

Lines changed: 92 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,12 @@
55
* 2.0.
66
*/
77

8-
import React, { Fragment, useState } from 'react';
8+
import React, { Fragment } from 'react';
99

10-
import {
11-
EuiButtonIcon,
12-
EuiFieldSearch,
13-
EuiIconTip,
14-
EuiSpacer,
15-
EuiTable,
16-
EuiTableBody,
17-
EuiTableHeader,
18-
EuiTableHeaderCell,
19-
EuiTableRow,
20-
EuiTableRowCell,
21-
EuiTextColor,
22-
} from '@elastic/eui';
23-
import { i18n } from '@kbn/i18n';
10+
import { EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
2411

2512
import { ASRoleMapping } from '../../app_search/types';
2613
import { WSRoleMapping } from '../../workplace_search/types';
27-
import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants';
2814
import { RoleRules } from '../types';
2915

3016
import './role_mappings_table.scss';
@@ -38,7 +24,9 @@ import {
3824
EXTERNAL_ATTRIBUTE_LABEL,
3925
ATTRIBUTE_VALUE_LABEL,
4026
FILTER_ROLE_MAPPINGS_PLACEHOLDER,
27+
ROLE_MAPPINGS_NO_RESULTS_MESSAGE,
4128
} from './constants';
29+
import { UsersAndRolesRowActions } from './users_and_roles_row_actions';
4230

4331
interface AccessItem {
4432
name: string;
@@ -58,8 +46,6 @@ interface Props {
5846
handleDeleteMapping(roleMappingId: string): void;
5947
}
6048

61-
const MAX_CELL_WIDTH = 24;
62-
6349
const noItemsPlaceholder = <EuiTextColor color="subdued">&mdash;</EuiTextColor>;
6450

6551
const getAuthProviderDisplayValue = (authProvider: string) =>
@@ -73,114 +59,104 @@ export const RoleMappingsTable: React.FC<Props> = ({
7359
initializeRoleMapping,
7460
handleDeleteMapping,
7561
}) => {
76-
const [filterValue, updateValue] = useState('');
62+
const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0];
63+
const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1];
7764

7865
// This is needed because App Search has `engines` and Workplace Search has `groups`.
79-
const standardizeRoleMapping = (roleMappings as SharedRoleMapping[]).map((rm) => {
66+
const standardizedRoleMappings = (roleMappings as SharedRoleMapping[]).map((rm) => {
8067
const _rm = { ...rm } as SharedRoleMapping;
8168
_rm.accessItems = rm[accessItemKey];
8269
return _rm;
83-
});
84-
85-
const filterResults = (result: SharedRoleMapping) => {
86-
// Filter out non-alphanumeric characters, except for underscores, hyphens, and spaces
87-
const sanitizedValue = filterValue.replace(/[^\w\s-]/g, '');
88-
const values = Object.values(result);
89-
const regexp = new RegExp(sanitizedValue, 'i');
90-
return values.filter((x) => regexp.test(x)).length > 0;
70+
}) as SharedRoleMapping[];
71+
72+
const attributeNameCol: EuiBasicTableColumn<SharedRoleMapping> = {
73+
field: 'attribute',
74+
name: EXTERNAL_ATTRIBUTE_LABEL,
75+
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules),
9176
};
9277

93-
const filteredResults = standardizeRoleMapping.filter(filterResults);
94-
const getFirstAttributeName = (rules: RoleRules): string => Object.entries(rules)[0][0];
95-
const getFirstAttributeValue = (rules: RoleRules): string => Object.entries(rules)[0][1];
78+
const attributeValueCol: EuiBasicTableColumn<SharedRoleMapping> = {
79+
field: 'attributeValue',
80+
name: ATTRIBUTE_VALUE_LABEL,
81+
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
82+
};
9683

97-
const rowActions = (id: string) => (
98-
<>
99-
<EuiButtonIcon
100-
onClick={() => initializeRoleMapping(id)}
101-
iconType="pencil"
102-
aria-label={MANAGE_BUTTON_LABEL}
103-
data-test-subj="ManageButton"
104-
/>{' '}
105-
<EuiButtonIcon
106-
onClick={() => handleDeleteMapping(id)}
107-
iconType="trash"
108-
aria-label={DELETE_BUTTON_LABEL}
109-
data-test-subj="DeleteButton"
84+
const roleCol: EuiBasicTableColumn<SharedRoleMapping> = {
85+
field: 'roleType',
86+
name: ROLE_LABEL,
87+
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
88+
};
89+
90+
const accessItemsCol: EuiBasicTableColumn<SharedRoleMapping> = {
91+
field: 'accessItems',
92+
name: accessHeader,
93+
render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => (
94+
<span data-test-subj="AccessItemsList">
95+
{accessAllEngines ? (
96+
ALL_LABEL
97+
) : (
98+
<>
99+
{accessItems.length === 0
100+
? noItemsPlaceholder
101+
: accessItems.map(({ name }) => (
102+
<Fragment key={name}>
103+
{name}
104+
<br />
105+
</Fragment>
106+
))}
107+
</>
108+
)}
109+
</span>
110+
),
111+
};
112+
113+
const authProviderCol: EuiBasicTableColumn<SharedRoleMapping> = {
114+
field: 'authProvider',
115+
name: AUTH_PROVIDER_LABEL,
116+
render: (_, { authProvider }: SharedRoleMapping) => (
117+
<span data-test-subj="AuthProviderDisplayValue">
118+
{authProvider.map(getAuthProviderDisplayValue).join(', ')}
119+
</span>
120+
),
121+
};
122+
123+
const actionsCol: EuiBasicTableColumn<SharedRoleMapping> = {
124+
field: 'id',
125+
name: '',
126+
align: 'right',
127+
render: (_, { id }: SharedRoleMapping) => (
128+
<UsersAndRolesRowActions
129+
onManageClick={() => initializeRoleMapping(id)}
130+
onDeleteClick={() => handleDeleteMapping(id)}
110131
/>
111-
</>
112-
);
132+
),
133+
};
113134

135+
const columns = shouldShowAuthProvider
136+
? [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, authProviderCol, actionsCol]
137+
: [attributeNameCol, attributeValueCol, roleCol, accessItemsCol, actionsCol];
138+
139+
const pagination = {
140+
hidePerPageOptions: true,
141+
};
142+
143+
const search = {
144+
box: {
145+
incremental: true,
146+
fullWidth: false,
147+
placeholder: FILTER_ROLE_MAPPINGS_PLACEHOLDER,
148+
'data-test-subj': 'RoleMappingsTableSearchInput',
149+
},
150+
};
114151
return (
115-
<>
116-
<EuiFieldSearch
117-
value={filterValue}
118-
placeholder={FILTER_ROLE_MAPPINGS_PLACEHOLDER}
119-
onChange={(e) => updateValue(e.target.value)}
120-
/>
121-
<EuiSpacer />
122-
{filteredResults.length > 0 ? (
123-
<EuiTable className="roleMappingsTable">
124-
<EuiTableHeader>
125-
<EuiTableHeaderCell>{EXTERNAL_ATTRIBUTE_LABEL}</EuiTableHeaderCell>
126-
<EuiTableHeaderCell>{ATTRIBUTE_VALUE_LABEL}</EuiTableHeaderCell>
127-
<EuiTableHeaderCell>{ROLE_LABEL}</EuiTableHeaderCell>
128-
<EuiTableHeaderCell>{accessHeader}</EuiTableHeaderCell>
129-
{shouldShowAuthProvider && (
130-
<EuiTableHeaderCell>{AUTH_PROVIDER_LABEL}</EuiTableHeaderCell>
131-
)}
132-
<EuiTableHeaderCell />
133-
</EuiTableHeader>
134-
<EuiTableBody>
135-
{filteredResults.map(
136-
({ id, authProvider, rules, roleType, accessAllEngines, accessItems, toolTip }) => (
137-
<EuiTableRow key={id}>
138-
<EuiTableRowCell>{getFirstAttributeName(rules)}</EuiTableRowCell>
139-
<EuiTableRowCell style={{ maxWidth: MAX_CELL_WIDTH }}>
140-
{getFirstAttributeValue(rules)}
141-
</EuiTableRowCell>
142-
<EuiTableRowCell>{roleType}</EuiTableRowCell>
143-
<EuiTableRowCell
144-
data-test-subj="AccessItemsList"
145-
style={{ maxWidth: MAX_CELL_WIDTH }}
146-
>
147-
{accessAllEngines ? (
148-
ALL_LABEL
149-
) : (
150-
<>
151-
{accessItems.length === 0
152-
? noItemsPlaceholder
153-
: accessItems.map(({ name }) => (
154-
<Fragment key={name}>
155-
{name}
156-
<br />
157-
</Fragment>
158-
))}
159-
</>
160-
)}
161-
</EuiTableRowCell>
162-
{shouldShowAuthProvider && (
163-
<EuiTableRowCell data-test-subj="AuthProviderDisplay">
164-
{authProvider.map(getAuthProviderDisplayValue).join(', ')}
165-
</EuiTableRowCell>
166-
)}
167-
<EuiTableRowCell align="right">
168-
{id && rowActions(id)}
169-
{toolTip && <EuiIconTip position="left" content={toolTip.content} />}
170-
</EuiTableRowCell>
171-
</EuiTableRow>
172-
)
173-
)}
174-
</EuiTableBody>
175-
</EuiTable>
176-
) : (
177-
<p>
178-
{i18n.translate('xpack.enterpriseSearch.roleMapping.moResults.message', {
179-
defaultMessage: "No results found for '{filterValue}'",
180-
values: { filterValue },
181-
})}
182-
</p>
183-
)}
184-
</>
152+
<EuiInMemoryTable
153+
data-test-subj="RoleMappingsTable"
154+
columns={columns}
155+
items={standardizedRoleMappings}
156+
search={search}
157+
pagination={pagination}
158+
message={ROLE_MAPPINGS_NO_RESULTS_MESSAGE}
159+
responsive={false}
160+
/>
185161
);
186162
};

0 commit comments

Comments
 (0)