19675: Merge branch '19675-instance-types-panel' from arvados-workbench2.git
[arvados.git] / services / workbench2 / 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 { getUserDetailsString, 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     tooltip: string;
21     uuid: string;
22 }
23
24 type ParticipantResource = GroupResource | UserResource;
25
26 interface ParticipantSelectProps {
27     items: Participant[];
28     excludedParticipants?: string[];
29     label?: string;
30     autofocus?: boolean;
31     onlyPeople?: boolean;
32     onlyActive?: boolean;
33     disabled?: boolean;
34
35     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
36     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
37     onCreate?: (person: Participant) => void;
38     onDelete?: (index: number) => void;
39     onSelect?: (person: Participant) => void;
40 }
41
42 interface ParticipantSelectState {
43     value: string;
44     suggestions: ParticipantResource[];
45 }
46
47 const getDisplayName = (item: GroupResource | UserResource, detailed: boolean) => {
48     switch (item.kind) {
49         case ResourceKind.USER:
50             return getUserDisplayName(item, detailed, detailed);
51         case ResourceKind.GROUP:
52             return item.name + `(${`(${(item as Resource).uuid})`})`;
53         default:
54             return (item as Resource).uuid;
55     }
56 };
57
58 const getDisplayTooltip = (item: GroupResource | UserResource) => {
59     switch (item.kind) {
60         case ResourceKind.USER:
61             return getUserDetailsString(item);
62         case ResourceKind.GROUP:
63             return item.name + `(${`(${(item as Resource).uuid})`})`;
64         default:
65             return (item as Resource).uuid;
66     }
67 };
68
69 export const ParticipantSelect = connect()(
70     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
71         state: ParticipantSelectState = {
72             value: '',
73             suggestions: []
74         };
75
76         render() {
77             const { label = 'Add people and groups' } = this.props;
78
79             return (
80                 <Autocomplete
81                     label={label}
82                     value={this.state.value}
83                     items={this.props.items}
84                     suggestions={this.state.suggestions}
85                     autofocus={this.props.autofocus}
86                     onChange={this.handleChange}
87                     onCreate={this.handleCreate}
88                     onSelect={this.handleSelect}
89                     onDelete={this.props.onDelete && !this.props.disabled ? this.handleDelete : undefined}
90                     onFocus={this.props.onFocus}
91                     onBlur={this.onBlur}
92                     renderChipValue={this.renderChipValue}
93                     renderChipTooltip={this.renderChipTooltip}
94                     renderSuggestion={this.renderSuggestion}
95                     disabled={this.props.disabled} />
96             );
97         }
98
99         onBlur = (e) => {
100             if (this.props.onBlur) {
101                 this.props.onBlur(e);
102             }
103             setTimeout(() => this.setState({ value: '', suggestions: [] }), 200);
104         }
105
106         renderChipValue(chipValue: Participant) {
107             const { name, uuid } = chipValue;
108             return name || uuid;
109         }
110
111         renderChipTooltip(item: Participant) {
112             return item.tooltip;
113         }
114
115         renderSuggestion(item: ParticipantResource) {
116             return (
117                 <ListItemText>
118                     <Typography noWrap>{getDisplayName(item, true)}</Typography>
119                 </ListItemText>
120             );
121         }
122
123         handleDelete = (_: Participant, index: number) => {
124             const { onDelete = noop } = this.props;
125             onDelete(index);
126         }
127
128         handleCreate = () => {
129             const { onCreate } = this.props;
130             if (onCreate) {
131                 this.setState({ value: '', suggestions: [] });
132                 onCreate({
133                     name: '',
134                     tooltip: '',
135                     uuid: this.state.value,
136                 });
137             }
138         }
139
140         handleSelect = (selection: ParticipantResource) => {
141             const { uuid } = selection;
142             const { onSelect = noop } = this.props;
143             this.setState({ value: '', suggestions: [] });
144             onSelect({
145                 name: getDisplayName(selection, false),
146                 tooltip: getDisplayTooltip(selection),
147                 uuid,
148             });
149         }
150
151         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
152             this.setState({ value: event.target.value }, this.getSuggestions);
153         }
154
155         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
156
157         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
158             const { value } = this.state;
159             const limit = 5; // FIXME: Does this provide a good UX?
160
161             const filterUsers = new FilterBuilder()
162                 .addILike('any', value)
163                 .addEqual('is_active', this.props.onlyActive || undefined)
164                 .addNotIn('uuid', this.props.excludedParticipants)
165                 .getFilters();
166             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
167
168             const filterGroups = new FilterBuilder()
169                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
170                 .addNotIn('uuid', this.props.excludedParticipants)
171                 .addILike('name', value)
172                 .getFilters();
173
174             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
175             this.setState({
176                 suggestions: this.props.onlyPeople
177                     ? userItems.items
178                     : userItems.items.concat(groupItems.items)
179             });
180         }
181     });