@truedat/auth 8.14.3 → 8.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +4 -4
  2. package/src/messages/en.js +6 -5
  3. package/src/messages/es.js +8 -7
  4. package/src/styles/UserCard.less +582 -0
  5. package/src/styles/UserModals.less +826 -0
  6. package/src/styles/Users.less +280 -0
  7. package/src/users/components/AdminUserRoutes.js +7 -20
  8. package/src/users/components/EditUser.js +7 -34
  9. package/src/users/components/EditUserModal.js +150 -0
  10. package/src/users/components/NewUser.js +128 -17
  11. package/src/users/components/Password.js +11 -189
  12. package/src/users/components/UserAcls.js +56 -6
  13. package/src/users/components/UserActions.js +99 -34
  14. package/src/users/components/UserCard.js +168 -22
  15. package/src/users/components/UserCards.js +273 -35
  16. package/src/users/components/UserFormFields.js +206 -0
  17. package/src/users/components/UserGroupAclRow.js +8 -2
  18. package/src/users/components/UserGroupAcls.js +2 -2
  19. package/src/users/components/UserGroupLink.js +21 -0
  20. package/src/users/components/UserInfoModal.js +268 -0
  21. package/src/users/components/UserPasswordField.js +66 -0
  22. package/src/users/components/UserPasswordModal.js +210 -0
  23. package/src/users/components/UserRoutes.js +4 -13
  24. package/src/users/components/UserTabs.js +5 -2
  25. package/src/users/components/UserTypeSelector.js +109 -0
  26. package/src/users/components/UsersAndGroups.js +39 -5
  27. package/src/users/components/UsersTable.js +234 -0
  28. package/src/users/components/__tests__/EditUser.spec.js +84 -10
  29. package/src/users/components/__tests__/NewUser.spec.js +34 -4
  30. package/src/users/components/__tests__/Password.spec.js +53 -146
  31. package/src/users/components/__tests__/UserAcls.spec.js +23 -2
  32. package/src/users/components/__tests__/UserCard.spec.js +111 -5
  33. package/src/users/components/__tests__/UserCards.spec.js +194 -0
  34. package/src/users/components/__tests__/UserGroupAclRow.spec.js +19 -1
  35. package/src/users/components/__tests__/UserModals.spec.js +507 -0
  36. package/src/users/components/__tests__/UserRoutes.spec.js +8 -3
  37. package/src/users/components/__tests__/UserTypeSelector.spec.js +32 -0
  38. package/src/users/components/__tests__/UsersAndGroups.spec.js +17 -0
  39. package/src/users/components/__tests__/__snapshots__/UserCard.spec.js.snap +64 -37
  40. package/src/users/components/__tests__/__snapshots__/UserCards.spec.js.snap +424 -75
  41. package/src/users/components/__tests__/__snapshots__/UserGroupAclRow.spec.js.snap +7 -3
  42. package/src/users/components/__tests__/__snapshots__/UserRoutes.spec.js.snap +4 -13
  43. package/src/users/hooks/useSortedGroups.js +13 -0
  44. package/src/users/hooks/useUserModalOperation.js +55 -0
  45. package/src/users/reducers/__tests__/userRedirect.spec.js +14 -3
  46. package/src/users/reducers/index.js +1 -0
  47. package/src/users/reducers/userModalOperation.js +29 -0
  48. package/src/users/reducers/userRedirect.js +5 -4
  49. package/src/users/sagas/__tests__/updateUser.spec.js +19 -1
  50. package/src/users/sagas/createUser.js +12 -5
  51. package/src/users/sagas/deleteUser.js +12 -5
  52. package/src/users/sagas/updateUser.js +19 -8
  53. package/src/users/selectors/__tests__/filterUsers.spec.js +11 -0
  54. package/src/users/selectors/__tests__/getUserAcls.spec.js +1 -0
  55. package/src/users/selectors/__tests__/getUserGroupAcls.spec.js +2 -0
  56. package/src/users/selectors/filterUsers.js +9 -1
  57. package/src/users/selectors/getUserGroupAcls.js +1 -0
  58. package/src/users/components/User.js +0 -68
  59. package/src/users/components/UserPassword.js +0 -24
  60. package/src/users/components/__tests__/User.spec.js +0 -20
  61. package/src/users/components/__tests__/UserPassword.spec.js +0 -24
  62. package/src/users/components/__tests__/__snapshots__/EditUser.spec.js.snap +0 -281
  63. package/src/users/components/__tests__/__snapshots__/NewUser.spec.js.snap +0 -321
  64. package/src/users/components/__tests__/__snapshots__/Password.spec.js.snap +0 -128
  65. package/src/users/components/__tests__/__snapshots__/User.spec.js.snap +0 -129
  66. package/src/users/components/__tests__/__snapshots__/UserPassword.spec.js.snap +0 -124
@@ -3,53 +3,293 @@ import { Fragment, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { connect } from "react-redux";
5
5
  import { Link } from "react-router";
6
- import { Button, Card, Header, Icon, Message } from "semantic-ui-react";
6
+ import {
7
+ Accordion,
8
+ Button,
9
+ Card,
10
+ Input,
11
+ Header,
12
+ Icon,
13
+ Label,
14
+ Message,
15
+ } from "semantic-ui-react";
7
16
  import { FormattedMessage, useIntl } from "react-intl";
8
- import SearchInput from "@truedat/core/components/SearchInput";
17
+ import SearchContext from "@truedat/core/search/SearchContext";
18
+ import SearchFilters from "@truedat/core/search/SearchFilters";
19
+ import FilterDropdown from "@truedat/core/components/FilterDropdown";
9
20
  import { USERS_NEW } from "@truedat/core/routes";
10
21
  import { filterUsers } from "../selectors/filterUsers";
11
22
  import UserCard from "./UserCard";
23
+ import UsersTable from "./UsersTable";
24
+ import { userTypeOptions } from "./UserTypeSelector";
12
25
 
13
- export const UserCards = ({ users: allUsers, showFullName }) => {
26
+ const ROLE_ORDER = ["admin", "user", "agent", "service"];
27
+ const rolePosition = (role = "") => {
28
+ const position = ROLE_ORDER.indexOf(_.toLower(role));
29
+ return position === -1 ? ROLE_ORDER.length : position;
30
+ };
31
+ const userSortKey = ({ full_name = "", user_name = "" }) =>
32
+ `${_.toLower(full_name)}|${_.toLower(user_name)}`;
33
+ const roleSort = ([roleA], [roleB]) => {
34
+ const posA = rolePosition(roleA);
35
+ const posB = rolePosition(roleB);
36
+ if (posA === posB && posA === ROLE_ORDER.length) {
37
+ return _.toLower(roleA).localeCompare(_.toLower(roleB));
38
+ }
39
+ return posA - posB;
40
+ };
41
+
42
+ export const UserCards = ({ users: allUsers }) => {
14
43
  const [filter, setFilter] = useState("");
44
+ const [selectedFilters, setSelectedFilters] = useState([]);
45
+ const [openFilter, setOpenFilter] = useState(null);
46
+ const [types, setTypes] = useState([]);
47
+ const [groups, setGroups] = useState([]);
48
+ const [sort, setSort] = useState({ column: "name", ascending: true });
49
+ const [view, setView] = useState(() => {
50
+ try {
51
+ return localStorage.getItem("truedat.users.view") === "table"
52
+ ? "table"
53
+ : "cards";
54
+ } catch {
55
+ return "cards";
56
+ }
57
+ });
58
+ const changeView = (value) => {
59
+ setView(value);
60
+ try {
61
+ localStorage.setItem("truedat.users.view", value);
62
+ } catch {
63
+ /* Storage may be unavailable. The selection remains local. */
64
+ }
65
+ };
66
+ const [openRoles, setOpenRoles] = useState({});
15
67
  const { formatMessage } = useIntl();
16
- const users = _.flow(_.sortBy("user_name"), filterUsers(filter))(allUsers);
68
+ const toggleRole = (role) => {
69
+ setOpenRoles((prev) => ({ ...prev, [role]: !(prev[role] ?? true) }));
70
+ };
71
+ const groupOptions = _.uniqBy("value")(
72
+ (allUsers || []).flatMap((user) =>
73
+ (user.groups || []).map((group) => ({
74
+ key: typeof group === "string" ? group : group.name,
75
+ value: typeof group === "string" ? group : group.name,
76
+ text: typeof group === "string" ? group : group.alias || group.name,
77
+ })),
78
+ ),
79
+ ).sort((a, b) => a.text.localeCompare(b.text));
80
+ const filterOptions = {
81
+ "users.type": userTypeOptions.map(({ value, messageId }) => ({
82
+ value,
83
+ text: formatMessage({ id: messageId }),
84
+ })),
85
+ "users.groups": groupOptions,
86
+ };
87
+ const resetFilters = () => {
88
+ setFilter("");
89
+ setTypes([]);
90
+ setGroups([]);
91
+ setSelectedFilters([]);
92
+ setOpenFilter(null);
93
+ };
94
+ const removeFilter = ({ filter: name }) => {
95
+ setSelectedFilters((values) => values.filter((value) => value !== name));
96
+ (name === "users.type" ? setTypes : setGroups)([]);
97
+ setOpenFilter(null);
98
+ };
99
+ const users = _.flow(
100
+ filterUsers(filter),
101
+ _.filter(
102
+ (user) =>
103
+ (!types.length || types.includes(user.role)) &&
104
+ (!groups.length ||
105
+ (user.groups || []).some((group) =>
106
+ groups.includes(typeof group === "string" ? group : group.name),
107
+ )),
108
+ ),
109
+ _.sortBy(userSortKey),
110
+ )(allUsers || []);
111
+ const hasFilters = Boolean(filter.trim() || types.length || groups.length);
17
112
  const usersByRole = _.flow(
18
- _.sortBy("role"),
19
113
  _.groupBy("role"),
20
114
  _.toPairs,
21
- )(users);
115
+ )(users).sort(roleSort);
22
116
 
23
117
  return (
24
118
  <>
25
- <SearchInput
26
- onChange={(_e, data) => _.flow(_.propOr("", "value"), setFilter)(data)}
27
- placeholder={formatMessage({ id: "user.search.placeholder" })}
28
- value={filter}
29
- />
30
- <Button
31
- floated="right"
32
- primary
33
- content={formatMessage({ id: "users.actions.create" })}
34
- icon="add user"
35
- as={Link}
36
- to={USERS_NEW}
37
- />
38
- {usersByRole.map(([role, users]) => (
39
- <Fragment key={role}>
40
- <Header
41
- as="h3"
42
- dividing
43
- content={formatMessage({ id: `user.type.${role}` })}
44
- />
45
- <Card.Group>
46
- {users.map((user, i) => (
47
- <UserCard key={i} user={user} showFullname={showFullName} />
119
+ <div className="users-toolbar">
120
+ <Input
121
+ iconPosition="left"
122
+ action
123
+ value={filter}
124
+ onChange={(_event, { value }) => setFilter(value)}
125
+ placeholder={formatMessage({ id: "user.search.placeholder" })}
126
+ >
127
+ <Icon name="search" aria-hidden="true" />
128
+ <input />
129
+ <SearchContext
130
+ value={{
131
+ availableGroupedFilters: [
132
+ [
133
+ "",
134
+ Object.keys(filterOptions).filter(
135
+ (name) => !selectedFilters.includes(name),
136
+ ),
137
+ ],
138
+ ],
139
+ addFilter: ({ filter: name }) => {
140
+ setSelectedFilters((values) => [...values, name]);
141
+ setOpenFilter(name);
142
+ },
143
+ resetFilters,
144
+ loadingFilters: false,
145
+ }}
146
+ >
147
+ <SearchFilters />
148
+ </SearchContext>
149
+ </Input>
150
+ <div className="users-toolbar-actions">
151
+ <div
152
+ className="users-view-buttons"
153
+ role="group"
154
+ aria-label={formatMessage({ id: "users.view.label" })}
155
+ >
156
+ {["cards", "table"].map((mode) => (
157
+ <Button
158
+ key={mode}
159
+ type="button"
160
+ toggle
161
+ active={view === mode}
162
+ aria-pressed={view === mode}
163
+ onClick={() => changeView(mode)}
164
+ icon={mode === "cards" ? "grid layout" : "list"}
165
+ content={formatMessage({ id: `users.view.${mode}` })}
166
+ />
48
167
  ))}
49
- </Card.Group>
50
- </Fragment>
51
- ))}
52
- {_.trim(filter) !== "" && _.isEmpty(users) ? (
168
+ </div>
169
+ <Button
170
+ primary
171
+ content={formatMessage({ id: "users.actions.create" })}
172
+ icon="add user"
173
+ as={Link}
174
+ to={USERS_NEW}
175
+ />
176
+ </div>
177
+ </div>
178
+ <div className="selectedFilters users-filters">
179
+ {selectedFilters.length > 0 && (
180
+ <div className="appliedFilters">
181
+ <FormattedMessage id="search.applied_filters" />
182
+ </div>
183
+ )}
184
+ {selectedFilters.map((name) => (
185
+ <FilterDropdown
186
+ key={name}
187
+ filter={name}
188
+ loading={false}
189
+ options={openFilter === name ? filterOptions[name] : null}
190
+ activeValues={name === "users.type" ? types : groups}
191
+ openFilter={() => setOpenFilter(name)}
192
+ closeFilter={() =>
193
+ setOpenFilter((current) => (current === name ? null : current))
194
+ }
195
+ removeFilter={removeFilter}
196
+ toggleFilterValue={({ value }) =>
197
+ (name === "users.type" ? setTypes : setGroups)((values) =>
198
+ values.includes(value)
199
+ ? values.filter((item) => item !== value)
200
+ : [...values, value],
201
+ )
202
+ }
203
+ />
204
+ ))}
205
+ {(hasFilters || selectedFilters.length > 0) && (
206
+ <a
207
+ className="resetFilters"
208
+ role="button"
209
+ tabIndex={0}
210
+ onKeyDown={(event) => {
211
+ if (event.key === "Enter" || event.key === " ") {
212
+ event.preventDefault();
213
+ event.currentTarget.click();
214
+ }
215
+ }}
216
+ onClick={resetFilters}
217
+ >
218
+ <FormattedMessage id="users.filter.clear" />
219
+ </a>
220
+ )}
221
+ </div>
222
+ <div className="users-results" role="status">
223
+ <Label>
224
+ {hasFilters && (
225
+ <>
226
+ <FormattedMessage
227
+ id="users.results.filtered"
228
+ values={{ count: users.length }}
229
+ />{" "}
230
+ </>
231
+ )}
232
+ <FormattedMessage
233
+ id="users.results"
234
+ values={{ count: (allUsers || []).length }}
235
+ />
236
+ </Label>
237
+ </div>
238
+ {view === "table" ? (
239
+ <UsersTable users={users} sort={sort} onSort={setSort} />
240
+ ) : (
241
+ <Accordion fluid styled className="users-role-accordion">
242
+ {usersByRole.map(([role, users]) => {
243
+ const active = openRoles[role] ?? true;
244
+ const panelId = `users-role-panel-${role}`;
245
+
246
+ return (
247
+ <Fragment key={role}>
248
+ <Accordion.Title
249
+ active={active}
250
+ className={`users-role-title users-role-title--${role}`}
251
+ >
252
+ <button
253
+ type="button"
254
+ className="users-role-toggle"
255
+ aria-expanded={active}
256
+ aria-controls={panelId}
257
+ onClick={() => toggleRole(role)}
258
+ >
259
+ <span className="users-role-title-content">
260
+ <Icon
261
+ className="users-role-chevron"
262
+ name={active ? "angle up" : "angle down"}
263
+ aria-hidden="true"
264
+ />
265
+ <Header as="h3" className="users-role-title-header">
266
+ <Header.Content>
267
+ {formatMessage({ id: `user.type.${role}` })}
268
+ </Header.Content>
269
+ </Header>
270
+ <Label circular className="users-role-count">
271
+ {users.length}
272
+ </Label>
273
+ </span>
274
+ </button>
275
+ </Accordion.Title>
276
+ <Accordion.Content
277
+ id={panelId}
278
+ active={active}
279
+ className="users-role-content"
280
+ >
281
+ <Card.Group className="users-role-card-group">
282
+ {users.map((user, i) => (
283
+ <UserCard key={user.id || i} user={user} />
284
+ ))}
285
+ </Card.Group>
286
+ </Accordion.Content>
287
+ </Fragment>
288
+ );
289
+ })}
290
+ </Accordion>
291
+ )}
292
+ {_.isEmpty(users) ? (
53
293
  <Message
54
294
  className="users-message"
55
295
  header={
@@ -66,13 +306,11 @@ export const UserCards = ({ users: allUsers, showFullName }) => {
66
306
 
67
307
  UserCards.propTypes = {
68
308
  users: PropTypes.array,
69
- showFullName: PropTypes.bool,
70
309
  };
71
310
 
72
311
  export const mapStateToProps = (state) => ({
73
312
  users: state.users,
74
313
  UserCard: state.userCard,
75
- showFullName: state.userCardFullName,
76
314
  });
77
315
 
78
316
  export default connect(mapStateToProps)(UserCards);
@@ -0,0 +1,206 @@
1
+ import PropTypes from "prop-types";
2
+ import { Form } from "semantic-ui-react";
3
+ import { useIntl } from "react-intl";
4
+ import { Controller } from "react-hook-form";
5
+ import UserTypeSelector from "./UserTypeSelector";
6
+
7
+ const isEmail = (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
8
+
9
+ const groupName = (group) => (typeof group === "string" ? group : group.name);
10
+
11
+ const buildGroupOptions = (groups = [], selected = []) => {
12
+ const fromGroups = groups.map((g, i) => ({
13
+ key: `g_${i}`,
14
+ text: typeof g === "string" ? g : g.alias || g.name || "",
15
+ value: typeof g === "string" ? g : g.name || "",
16
+ }));
17
+ const existing = groups.map(groupName);
18
+ const fromSelected = (selected || [])
19
+ .filter((v) => typeof v === "string" && !existing.includes(v))
20
+ .filter((v, i, a) => a.indexOf(v) === i)
21
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
22
+ .map((text, i) => ({ key: `s_${i}`, text, value: text }));
23
+ return [...fromGroups, ...fromSelected];
24
+ };
25
+
26
+ const TextField = ({
27
+ control,
28
+ errors,
29
+ name,
30
+ rules,
31
+ disabled,
32
+ icon,
33
+ required,
34
+ }) => {
35
+ const { formatMessage } = useIntl();
36
+ return (
37
+ <Controller
38
+ control={control}
39
+ name={name}
40
+ rules={rules}
41
+ render={({ field: { onBlur, onChange, value, ref } }) => (
42
+ <Form.Input
43
+ name={name}
44
+ input={{ ref }}
45
+ disabled={disabled}
46
+ id={name}
47
+ autoComplete="off"
48
+ {...(icon ? { icon, iconPosition: "left" } : {})}
49
+ error={errors[name]?.message}
50
+ label={{
51
+ children: formatMessage({ id: `user.form.${name}` }),
52
+ htmlFor: name,
53
+ }}
54
+ onBlur={onBlur}
55
+ onChange={(_e, { value: v }) => onChange(v)}
56
+ placeholder={formatMessage({ id: `user.form.placeholder.${name}` })}
57
+ value={value}
58
+ required={required}
59
+ />
60
+ )}
61
+ />
62
+ );
63
+ };
64
+
65
+ TextField.propTypes = {
66
+ control: PropTypes.object.isRequired,
67
+ errors: PropTypes.object.isRequired,
68
+ name: PropTypes.string.isRequired,
69
+ rules: PropTypes.object,
70
+ disabled: PropTypes.bool,
71
+ icon: PropTypes.string,
72
+ required: PropTypes.bool,
73
+ };
74
+
75
+ const Section = ({ barClass, labelId, children }) => {
76
+ const { formatMessage } = useIntl();
77
+ return (
78
+ <>
79
+ <div className="um-section-header">
80
+ <div className={`um-section-bar um-section-bar--${barClass}`} />
81
+ <span className="um-section-label">
82
+ {formatMessage({ id: labelId })}
83
+ </span>
84
+ </div>
85
+ {children}
86
+ </>
87
+ );
88
+ };
89
+
90
+ Section.propTypes = {
91
+ barClass: PropTypes.string.isRequired,
92
+ labelId: PropTypes.string.isRequired,
93
+ children: PropTypes.node,
94
+ };
95
+
96
+ // Personal, type and account fields shared by the create and edit user modals.
97
+ // Both modals validate and focus the first invalid field the same way.
98
+ export const UserFormFields = ({ control, errors, isSubmitting, groups }) => {
99
+ const { formatMessage } = useIntl();
100
+ const requiredMessage = (field) =>
101
+ formatMessage(
102
+ { id: "form.validation.required" },
103
+ { prop: formatMessage({ id: `user.form.${field}` }) },
104
+ );
105
+
106
+ return (
107
+ <>
108
+ <div className="um-form-top um-section">
109
+ <div className="um-section">
110
+ <Section barClass="personal" labelId="user.section.personal">
111
+ <div className="um-personal-fields">
112
+ <TextField
113
+ control={control}
114
+ errors={errors}
115
+ name="full_name"
116
+ rules={{ required: requiredMessage("full_name") }}
117
+ disabled={isSubmitting}
118
+ icon="user"
119
+ required
120
+ />
121
+ <TextField
122
+ control={control}
123
+ errors={errors}
124
+ name="email"
125
+ rules={{
126
+ validate: (v) =>
127
+ isEmail(v) ||
128
+ formatMessage({ id: "form.validation.email.invalid" }),
129
+ }}
130
+ disabled={isSubmitting}
131
+ icon="at"
132
+ />
133
+ </div>
134
+ </Section>
135
+ </div>
136
+ <Controller
137
+ control={control}
138
+ name="role"
139
+ render={({ field: { onBlur, onChange, value } }) => (
140
+ <UserTypeSelector
141
+ disabled={isSubmitting}
142
+ id="role"
143
+ label={formatMessage({ id: "user.form.role" })}
144
+ onBlur={onBlur}
145
+ onChange={onChange}
146
+ value={value || "user"}
147
+ required
148
+ />
149
+ )}
150
+ />
151
+ </div>
152
+ <div className="um-section">
153
+ <Section barClass="account" labelId="user.section.account">
154
+ <div className="um-form-grid">
155
+ <TextField
156
+ control={control}
157
+ errors={errors}
158
+ name="user_name"
159
+ rules={{ required: requiredMessage("user_name") }}
160
+ disabled={isSubmitting}
161
+ icon="user circle"
162
+ required
163
+ />
164
+ <TextField
165
+ control={control}
166
+ errors={errors}
167
+ name="external_id"
168
+ disabled={isSubmitting}
169
+ />
170
+ </div>
171
+ </Section>
172
+ <Controller
173
+ control={control}
174
+ name="groups"
175
+ render={({ field: { onBlur, onChange, value } }) => (
176
+ <Form.Dropdown
177
+ disabled={isSubmitting}
178
+ id="groups"
179
+ label={formatMessage({ id: "user.form.groups" })}
180
+ search
181
+ selection
182
+ multiple
183
+ allowAdditions
184
+ options={buildGroupOptions(groups, value)}
185
+ onBlur={onBlur}
186
+ onChange={(_e, { value: v }) => onChange(v)}
187
+ placeholder={formatMessage({
188
+ id: "user.form.placeholder.groups",
189
+ })}
190
+ value={value}
191
+ />
192
+ )}
193
+ />
194
+ </div>
195
+ </>
196
+ );
197
+ };
198
+
199
+ UserFormFields.propTypes = {
200
+ control: PropTypes.object.isRequired,
201
+ errors: PropTypes.object.isRequired,
202
+ isSubmitting: PropTypes.bool,
203
+ groups: PropTypes.array,
204
+ };
205
+
206
+ export default UserFormFields;
@@ -1,11 +1,16 @@
1
1
  import PropTypes from "prop-types";
2
2
  import { Table } from "semantic-ui-react";
3
3
  import { DomainTooltip } from "@truedat/bg/taxonomy/components/DomainPath";
4
+ import UserGroupLink from "./UserGroupLink";
4
5
 
5
- export const UserGroupAclRow = ({ resource, role, group }) => (
6
+ export const UserGroupAclRow = ({ resource, role, group, groupId }) => (
6
7
  <Table.Row>
8
+ <Table.Cell>
9
+ <span className="um-group-chip">
10
+ <UserGroupLink id={groupId} name={group} />
11
+ </span>
12
+ </Table.Cell>
7
13
  <Table.Cell content={<DomainTooltip domain={resource} />} />
8
- <Table.Cell content={group} />
9
14
  <Table.Cell content={role} />
10
15
  </Table.Row>
11
16
  );
@@ -14,6 +19,7 @@ UserGroupAclRow.propTypes = {
14
19
  resource: PropTypes.object,
15
20
  role: PropTypes.string,
16
21
  group: PropTypes.string,
22
+ groupId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
17
23
  };
18
24
 
19
25
  export default UserGroupAclRow;
@@ -15,10 +15,10 @@ export const UserGroupAcls = ({ acls }) => {
15
15
  <Table.Header>
16
16
  <Table.Row>
17
17
  <Table.HeaderCell
18
- content={formatMessage({ id: "user.acl.domain" })}
18
+ content={formatMessage({ id: "user.acl.group" })}
19
19
  />
20
20
  <Table.HeaderCell
21
- content={formatMessage({ id: "user.acl.group" })}
21
+ content={formatMessage({ id: "user.acl.domain" })}
22
22
  />
23
23
  <Table.HeaderCell
24
24
  content={formatMessage({ id: "user.acl.role" })}
@@ -0,0 +1,21 @@
1
+ import PropTypes from "prop-types";
2
+ import { Icon } from "semantic-ui-react";
3
+ import { Link } from "react-router";
4
+ import { linkTo } from "@truedat/core/routes";
5
+
6
+ const UserGroupLink = ({ id, name }) =>
7
+ id != null ? (
8
+ <Link to={linkTo.GROUP({ id })} target="_blank" rel="noopener noreferrer">
9
+ <Icon name="external alternate" aria-hidden="true" />
10
+ <span>{name}</span>
11
+ </Link>
12
+ ) : (
13
+ name
14
+ );
15
+
16
+ UserGroupLink.propTypes = {
17
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
18
+ name: PropTypes.string,
19
+ };
20
+
21
+ export default UserGroupLink;