19231: Add smaller page sizes (10 and 20 items) to load faster
[arvados-workbench2.git] / src / views-components / sharing-dialog / participant-select.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { Autocomplete } from 'components/autocomplete/autocomplete';
7 import { connect, DispatchProp } from 'react-redux';
8 import { ServiceRepository } from 'services/services';
9 import { FilterBuilder } from '../../services/api/filter-builder';
10 import { debounce } from 'debounce';
11 import { ListItemText, Typography } from '@material-ui/core';
12 import { noop } from 'lodash/fp';
13 import { GroupClass, GroupResource } from 'models/group';
14 import { getUserDisplayName, UserResource } from 'models/user';
15 import { Resource, ResourceKind } from 'models/resource';
16 import { ListResults } from 'services/common-service/common-service';
17
18 export interface Participant {
19     name: string;
20     uuid: string;
21 }
22
23 type ParticipantResource = GroupResource | UserResource;
24
25 interface ParticipantSelectProps {
26     items: Participant[];
27     excludedParticipants?: string[];
28     label?: string;
29     autofocus?: boolean;
30     onlyPeople?: boolean;
31     onlyActive?: boolean;
32     disabled?: boolean;
33
34     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
35     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
36     onCreate?: (person: Participant) => void;
37     onDelete?: (index: number) => void;
38     onSelect?: (person: Participant) => void;
39 }
40
41 interface ParticipantSelectState {
42     value: string;
43     suggestions: ParticipantResource[];
44 }
45
46 const getDisplayName = (item: GroupResource | UserResource) => {
47     switch (item.kind) {
48         case ResourceKind.USER:
49             return getUserDisplayName(item, true, true);
50         case ResourceKind.GROUP:
51             return item.name + `(${`(${(item as Resource).uuid})`})`;
52         default:
53             return (item as Resource).uuid;
54     }
55 };
56
57 export const ParticipantSelect = connect()(
58     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
59         state: ParticipantSelectState = {
60             value: '',
61             suggestions: []
62         };
63
64         render() {
65             const { label = 'Share' } = this.props;
66
67             return (
68                 <Autocomplete
69                     label={label}
70                     value={this.state.value}
71                     items={this.props.items}
72                     suggestions={this.state.suggestions}
73                     autofocus={this.props.autofocus}
74                     onChange={this.handleChange}
75                     onCreate={this.handleCreate}
76                     onSelect={this.handleSelect}
77                     onDelete={this.props.onDelete && !this.props.disabled ? this.handleDelete : undefined}
78                     onFocus={this.props.onFocus}
79                     onBlur={this.props.onBlur}
80                     renderChipValue={this.renderChipValue}
81                     renderSuggestion={this.renderSuggestion}
82                     disabled={this.props.disabled}/>
83             );
84         }
85
86         renderChipValue(chipValue: Participant) {
87             const { name, uuid } = chipValue;
88             return name || uuid;
89         }
90
91         renderSuggestion(item: ParticipantResource) {
92             return (
93                 <ListItemText>
94                     <Typography noWrap>{getDisplayName(item)}</Typography>
95                 </ListItemText>
96             );
97         }
98
99         handleDelete = (_: Participant, index: number) => {
100             const { onDelete = noop } = this.props;
101             onDelete(index);
102         }
103
104         handleCreate = () => {
105             const { onCreate } = this.props;
106             if (onCreate) {
107                 this.setState({ value: '', suggestions: [] });
108                 onCreate({
109                     name: '',
110                     uuid: this.state.value,
111                 });
112             }
113         }
114
115         handleSelect = (selection: ParticipantResource) => {
116             const { uuid } = selection;
117             const { onSelect = noop } = this.props;
118             this.setState({ value: '', suggestions: [] });
119             onSelect({
120                 name: getDisplayName(selection),
121                 uuid,
122             });
123         }
124
125         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
126             this.setState({ value: event.target.value }, this.getSuggestions);
127         }
128
129         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
130
131         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
132             const { value } = this.state;
133             const limit = 5; // FIXME: Does this provide a good UX?
134
135             const filterUsers = new FilterBuilder()
136                 .addILike('any', value)
137                 .addEqual('is_active', this.props.onlyActive || undefined)
138                 .addNotIn('uuid', this.props.excludedParticipants)
139                 .getFilters();
140             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
141
142             const filterGroups = new FilterBuilder()
143                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
144                 .addNotIn('uuid', this.props.excludedParticipants)
145                 .addILike('name', value)
146                 .getFilters();
147
148             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
149             this.setState({
150                 suggestions: this.props.onlyPeople
151                     ? userItems.items
152                     : userItems.items.concat(groupItems.items)
153             });
154         }
155     });