Skip to content

Commit

Permalink
[Enterprise Search] Refactor RoleMappingsTable to use EuiInMemoryTable (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
scottybollinger authored Jun 10, 2021
1 parent 61677f7 commit 797c0c9
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 auth provider display names', () => {
const wrapper = shallow(<RoleMappingsTable {...props} />);
it('renders with "shouldShowAuthProvider" false', () => {
const wrapper = mount(<RoleMappingsTable {...props} shouldShowAuthProvider={false} />);

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

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

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

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" />
);

Expand All @@ -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]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -58,8 +46,6 @@ interface Props {
handleDeleteMapping(roleMappingId: string): void;
}

const MAX_CELL_WIDTH = 24;

const noItemsPlaceholder = <EuiTextColor color="subdued">&mdash;</EuiTextColor>;

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

// This is needed because App Search has `engines` and Workplace Search has `groups`.
const standardizeRoleMapping = (roleMappings as SharedRoleMapping[]).map((rm) => {
const standardizedRoleMappings = (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;
}) as SharedRoleMapping[];

const attributeNameCol: EuiBasicTableColumn<SharedRoleMapping> = {
field: 'attribute',
name: EXTERNAL_ATTRIBUTE_LABEL,
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules),
};

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 attributeValueCol: EuiBasicTableColumn<SharedRoleMapping> = {
field: 'attributeValue',
name: ATTRIBUTE_VALUE_LABEL,
render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
};

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"
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)}
/>
</>
);
),
};

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 (
<>
<EuiFieldSearch
value={filterValue}
placeholder={FILTER_ROLE_MAPPINGS_PLACEHOLDER}
onChange={(e) => updateValue(e.target.value)}
/>
<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>
)}
</>
<EuiInMemoryTable
data-test-subj="RoleMappingsTable"
columns={columns}
items={standardizedRoleMappings}
search={search}
pagination={pagination}
message={ROLE_MAPPINGS_NO_RESULTS_MESSAGE}
responsive={false}
/>
);
};
Loading

0 comments on commit 797c0c9

Please sign in to comment.