Skip to content

Commit

Permalink
Support unknown users
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasikas committed Dec 13, 2022
1 parent ebda905 commit 9bf0b69
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
EuiTitle,
} from '@elastic/eui';

import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { useBulkGetUserProfiles } from '../../../containers/user_profiles/use_bulk_get_user_profiles';
import type { Case } from '../../../../common';
import { EditAssigneesSelectable } from './edit_assignees_selectable';
Expand All @@ -42,6 +43,25 @@ const fullHeight = css`
}
`;

const getUnknownUsers = (
assignees: Set<string>,
userProfiles?: Map<string, UserProfileWithAvatar>
) => {
const unknownUsers: string[] = [];

if (!userProfiles) {
return unknownUsers;
}

for (const assignee of assignees) {
if (!userProfiles.has(assignee)) {
unknownUsers.push(assignee);
}
}

return unknownUsers;
};

const EditAssigneesFlyoutComponent: React.FC<Props> = ({
selectedCases,
onClose,
Expand All @@ -56,6 +76,8 @@ const EditAssigneesFlyoutComponent: React.FC<Props> = ({
uids: Array.from(assignees.values()),
});

const unknownUsers = getUnknownUsers(assignees, userProfiles);

const [assigneesSelection, setAssigneesSelection] = useState<ItemsSelectionState>({
selectedItems: [],
unSelectedItems: [],
Expand Down Expand Up @@ -95,6 +117,7 @@ const EditAssigneesFlyoutComponent: React.FC<Props> = ({
isLoading={isLoadingUserProfiles}
userProfiles={userProfiles ?? new Map()}
onChangeAssignees={setAssigneesSelection}
unknownUsers={unknownUsers}
/>
)}
</EuiFlyoutBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('EditAssigneesSelectable', () => {
isLoading: false,
userProfiles: userProfilesMap,
onChangeAssignees: jest.fn(),
unknownUsers: [],
};

/**
Expand All @@ -43,6 +44,7 @@ describe('EditAssigneesSelectable', () => {
],
isLoading: false,
userProfiles: userProfilesMap,
unknownUsers: [],
onChangeAssignees: jest.fn(),
};

Expand Down Expand Up @@ -361,4 +363,51 @@ describe('EditAssigneesSelectable', () => {

await waitForComponentToUpdate();
});

it('shows unknown users', async () => {
const result = appMock.render(<EditAssigneesSelectable {...props} unknownUsers={['123']} />);

await waitFor(() => {
expect(result.getByText('Unknown')).toBeInTheDocument();
});

await waitForComponentToUpdate();
});

it('selects unknown users', async () => {
const result = appMock.render(<EditAssigneesSelectable {...props} unknownUsers={['123']} />);

await waitFor(() => {
expect(result.getByText('Unknown')).toBeInTheDocument();
});

userEvent.click(result.getByText('Unknown'));

expect(props.onChangeAssignees).toBeCalledWith({
selectedItems: ['u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', '123'],
unSelectedItems: [],
});

await waitForComponentToUpdate();
});

it('deselects unknown users', async () => {
const selectedCases = [{ ...basicCase, assignees: [{ uid: '123' }] }];
const result = appMock.render(
<EditAssigneesSelectable {...props} selectedCases={selectedCases} unknownUsers={['123']} />
);

await waitFor(() => {
expect(result.getByText('Unknown')).toBeInTheDocument();
});

userEvent.click(result.getByText('Unknown'));

expect(props.onChangeAssignees).toBeCalledWith({
selectedItems: [],
unSelectedItems: ['123'],
});

await waitForComponentToUpdate();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '@elastic/eui';

import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
import { UserAvatar, getUserDisplayName } from '@kbn/user-profile-components';
import { getUserDisplayName } from '@kbn/user-profile-components';
import { useIsUserTyping } from '../../../common/use_is_user_typing';
import { useSuggestUserProfiles } from '../../../containers/user_profiles/use_suggest_user_profiles';
import type { Case } from '../../../../common';
Expand All @@ -31,27 +31,32 @@ import type { ItemSelectableOption, ItemsSelectionState } from '../types';
import { useCasesContext } from '../../cases_context/use_cases_context';
import { EmptyMessage } from '../../user_profiles/empty_message';
import { NoMatches } from '../../user_profiles/no_matches';
import { SmallUserAvatar } from '../../user_profiles/small_user_avatar';

interface Props {
selectedCases: Case[];
userProfiles: Map<string, UserProfileWithAvatar>;
isLoading: boolean;
unknownUsers: string[];
onChangeAssignees: (args: ItemsSelectionState) => void;
}

type AssigneeSelectableOption = ItemSelectableOption<Partial<UserProfileWithAvatar>>;
type AssigneeSelectableOption = ItemSelectableOption<
Partial<UserProfileWithAvatar> & { unknownUser?: boolean }
>;

const EditAssigneesSelectableComponent: React.FC<Props> = ({
selectedCases,
userProfiles,
isLoading,
unknownUsers,
onChangeAssignees,
}) => {
const { owner: owners } = useCasesContext();
const { euiTheme } = useEuiTheme();
const { isUserTyping, onContentChange, onDebounce } = useIsUserTyping();
// TODO: Include unknown users
const userProfileIds = [...userProfiles.keys()];

const userProfileIds = [...userProfiles.keys(), ...unknownUsers];

const [searchValue, setSearchValue] = useState<string>('');
const { data: searchResultUserProfiles, isLoading: isLoadingSuggest } = useSuggestUserProfiles({
Expand All @@ -62,13 +67,12 @@ const EditAssigneesSelectableComponent: React.FC<Props> = ({

const itemToSelectableOption = useCallback(
(item: { key: string; data: Record<string, unknown> }): AssigneeSelectableOption => {
// TODO: Fix types
const userProfileFromData = item.data as unknown as UserProfileWithAvatar;
const userProfileFromData = item.data as unknown as Partial<UserProfileWithAvatar>;
const userProfile = isEmpty(userProfileFromData)
? userProfiles.get(item.key)
: userProfileFromData;

if (userProfile) {
if (isUserProfile(userProfile)) {
return toSelectableOption(userProfile);
}

Expand All @@ -80,12 +84,12 @@ const EditAssigneesSelectableComponent: React.FC<Props> = ({
return toSelectableOption(profileInSuggestedUsers);
}

// TODO: Put unknown label
return {
key: item.key,
label: item.key,
label: i18n.UNKNOWN,
data: { unknownUser: true },
'data-test-subj': `cases-actions-assignees-edit-selectable-assignee-${item.key}`,
} as AssigneeSelectableOption;
} as unknown as AssigneeSelectableOption;
},
[searchResultUserProfiles, userProfiles]
);
Expand All @@ -111,10 +115,7 @@ const EditAssigneesSelectableComponent: React.FC<Props> = ({
(option: AssigneeSelectableOption, search: string) => {
const icon = option.itemIcon ?? 'empty';
const dataTestSubj = `cases-actions-assignees-edit-selectable-assignee-${option.key}-icon-${icon}`;

if (!option.user) {
return <EuiHighlight search={searchValue}>{option.label}</EuiHighlight>;
}
const userInfo = option.user ? { user: option.user, data: option.data } : undefined;

return (
<EuiFlexGroup>
Expand All @@ -129,7 +130,7 @@ const EditAssigneesSelectableComponent: React.FC<Props> = ({
<EuiIcon type={icon} data-test-subj={dataTestSubj} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<UserAvatar user={option.user} avatar={option.data?.avatar} size="s" />
<SmallUserAvatar userInfo={userInfo} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
Expand All @@ -142,7 +143,7 @@ const EditAssigneesSelectableComponent: React.FC<Props> = ({
<EuiFlexItem>
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
</EuiFlexItem>
{option.user.email && option.user.email !== option.label ? (
{option.user?.email && option.user?.email !== option.label ? (
<EuiFlexItem grow={false}>
<EuiTextColor color={option.disabled ? 'disabled' : 'subdued'}>
{searchValue ? (
Expand Down Expand Up @@ -250,11 +251,16 @@ const getDisplayOptions = ({
/**
* In the initial view, when the user does not perform any search,
* we want to filter out options that are not in the initial user profile
* mapping or profiles returned by the search result that are not selected
* mapping or profiles returned by the search result that are not selected.
* We want to keep unknown users as they can only be available from the
* selected cases and not from search results
*/
const filteredOptions = isEmpty(searchValue)
? options.filter(
(option) => initialUserProfiles.has(option?.data?.uid) || option?.data?.itemIcon !== 'empty'
(option) =>
initialUserProfiles.has(option?.data?.uid) ||
option?.data?.itemIcon !== 'empty' ||
option.data?.unknownUser
)
: [...options];

Expand Down Expand Up @@ -285,3 +291,7 @@ const isMatchingOption = <Option extends UserProfileWithAvatar | null>(
) => {
return option.key === profile.uid;
};

const isUserProfile = (
userProfile?: Partial<UserProfileWithAvatar>
): userProfile is UserProfileWithAvatar => !!userProfile && !!userProfile.uid && !!userProfile.user;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
export { CANCEL } from '../../../common/translations';
export { CANCEL, UNKNOWN } from '../../../common/translations';
export { EDITED_CASES, SELECTED_CASES, SAVE_SELECTION, SEARCH_PLACEHOLDER } from '../translations';

export const EDIT_ASSIGNEES = i18n.translate('xpack.cases.actions.assignees.edit', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ConnectorsComponent: React.FC<Props> = ({
const connectorsName = connector?.name ?? 'none';

const actionTypeName = useMemo(
() => actionTypes.find((c) => c.id === selectedConnector.type)?.name ?? 'Unknown',
() => actionTypes.find((c) => c.id === selectedConnector.type)?.name ?? i18n.UNKNOWN,
[actionTypes, selectedConnector.type]
);

Expand Down

0 comments on commit 9bf0b69

Please sign in to comment.