@rpcbase/client 0.214.0 → 0.215.0

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 (26) hide show
  1. package/access-control/ACLForm/components/GrantField/OpSelector.tsx +129 -0
  2. package/access-control/ACLForm/components/GrantField/ResourceSelector.tsx +86 -0
  3. package/access-control/ACLForm/components/GrantField/UsersSelector.tsx +96 -0
  4. package/access-control/ACLForm/components/GrantField/grant-field.scss +26 -0
  5. package/access-control/ACLForm/components/GrantField/icons/CheckMark.tsx +16 -0
  6. package/access-control/ACLForm/components/GrantField/icons/CollapseArrow.tsx +14 -0
  7. package/access-control/ACLForm/components/GrantField/icons/ExpandArrow.tsx +14 -0
  8. package/access-control/ACLForm/components/GrantField/index.tsx +91 -0
  9. package/access-control/ACLForm/components/GrantsList.tsx +48 -0
  10. package/access-control/ACLForm/components/RoleForm.tsx +134 -0
  11. package/access-control/ACLForm/components/RoleView.tsx +115 -0
  12. package/access-control/ACLForm/components/RolesList.tsx +79 -0
  13. package/access-control/ACLForm/components/constants.tsx +1 -0
  14. package/access-control/ACLForm/components/resolver.ts +57 -0
  15. package/access-control/ACLForm/components/role-form.scss +19 -0
  16. package/access-control/ACLForm/index.tsx +48 -0
  17. package/access-control/ACLModal/acl-modal.scss +7 -0
  18. package/access-control/ACLModal/index.tsx +66 -0
  19. package/access-control/index.ts +2 -0
  20. package/firebase/index.js +1 -1
  21. package/firebase/sw.js +1 -1
  22. package/package.json +10 -10
  23. package/ui/SelectPills/index.tsx +92 -0
  24. package/ui/SelectPills/select-pills.scss +66 -0
  25. package/ui/icons/Close.tsx +14 -0
  26. package/ui/icons/index.tsx +1 -0
@@ -0,0 +1,129 @@
1
+ import _get from "lodash/get"
2
+ import {useState} from "react"
3
+ import ToggleButton from "react-bootstrap/ToggleButton"
4
+ import ToggleButtonGroup from "react-bootstrap/ToggleButtonGroup"
5
+
6
+ import {useFormContext} from "react-hook-form"
7
+
8
+ import {GRANTS_FIELD} from "../constants"
9
+ import {CheckMark} from "./icons/CheckMark"
10
+
11
+
12
+ const OPS = {
13
+ create: "Create",
14
+ read: "Read",
15
+ update: "Update",
16
+ delete: "Delete",
17
+ }
18
+
19
+ export const OpSelector = ({field, index, update}) => {
20
+ const PREFIX = `select-op-field-${field.id}`
21
+
22
+ const {
23
+ formState: {errors},
24
+ setValue,
25
+ } = useFormContext()
26
+
27
+ const fieldKey = `${GRANTS_FIELD}.${index}.ops`
28
+ const err = _get(errors, fieldKey, null)
29
+
30
+ const [ops, setOps] = useState(field.ops || {})
31
+
32
+ const updateOps = (nextOps) => {
33
+ setOps(nextOps)
34
+ setValue(fieldKey, nextOps, {
35
+ shouldTouch: true,
36
+ shouldValidate: !!err, // revalidate if there is an error
37
+ })
38
+ }
39
+
40
+ const handleChange = (selected) => {
41
+ const nextOps = {...ops}
42
+ // remove unselected keys from ops
43
+ Object.keys(nextOps).forEach((k) => !selected.includes[k] && delete nextOps[k])
44
+ // add new selected keys
45
+ selected.forEach((s) => (nextOps[s] = "own"))
46
+ updateOps(nextOps)
47
+ }
48
+
49
+ const getCRUDOps = () => Object.keys(ops)
50
+
51
+ const getOpScope = (k) => ops[k]
52
+
53
+ const updateOpScope = (k, selected) => {
54
+ const nextOps = {...ops}
55
+ nextOps[k] = selected
56
+ updateOps(nextOps)
57
+ }
58
+
59
+ const getCaption = () => {
60
+ if (err) {
61
+ return <p className="text-danger pt-1">{err.message}</p>
62
+ } else {
63
+ // iterate on OPS first, not ops to preserve semantic order
64
+ const targetOps = Object.keys(OPS).filter((k) => Object.keys(ops).includes(k))
65
+ let captionStr
66
+ if (targetOps.length === 0) {
67
+ captionStr = "Select operations above"
68
+ } else captionStr = `Allow ${targetOps.map((k) => `${OPS[k]}:${ops[k]}`).join(", ")}`
69
+ return <p className="text-secondary mt-1">{captionStr}</p>
70
+ }
71
+ }
72
+
73
+ return (
74
+ <div className="op-selector">
75
+ <div className="mb-1 fw-bold">Grant Operation:</div>
76
+ <ToggleButtonGroup
77
+ type="checkbox"
78
+ // TODO: support vertical layout when the modal isn't wide enough
79
+ // vertical
80
+ className={cx("w-100", {"border": !!err, "border-danger": !!err})}
81
+ value={getCRUDOps()}
82
+ onChange={handleChange}
83
+ >
84
+ {Object.keys(OPS).map((k) => (
85
+ <ToggleButton
86
+ variant="light"
87
+ key={`${PREFIX}-${index}-${k}`}
88
+ id={`${PREFIX}-${index}-${k}`}
89
+ value={k}
90
+ className="toggle-button-op py-1"
91
+ >
92
+ <>
93
+ <div className="">
94
+ <CheckMark hide={!getCRUDOps().includes(k)} />
95
+ {OPS[k]}
96
+ </div>
97
+ <ToggleButtonGroup
98
+ className="pt-1"
99
+ type="radio"
100
+ name={`${k}_op_scope`}
101
+ value={getOpScope(k)}
102
+ onChange={(selected) => updateOpScope(k, selected)}
103
+ >
104
+ <ToggleButton
105
+ variant="primary"
106
+ id={`${PREFIX}-${index}-${k}-own`}
107
+ value={"own"}
108
+ className="toggle-button-op py-1 text-monospace"
109
+ >
110
+ own
111
+ </ToggleButton>
112
+ <ToggleButton
113
+ variant="primary"
114
+ id={`${PREFIX}-${index}-${k}-any`}
115
+ value={"any"}
116
+ className="toggle-button-op py-1 text-monospace"
117
+ >
118
+ any
119
+ </ToggleButton>
120
+ </ToggleButtonGroup>
121
+ </>
122
+ </ToggleButton>
123
+ ))}
124
+ </ToggleButtonGroup>
125
+
126
+ {getCaption()}
127
+ </div>
128
+ )
129
+ }
@@ -0,0 +1,86 @@
1
+ /* @flow */
2
+ import _get from "lodash/get"
3
+ import {useState, useEffect} from "react"
4
+
5
+ import {useFormContext} from "react-hook-form"
6
+ import {Typeahead} from "react-bootstrap-typeahead"
7
+ import Form from "react-bootstrap/Form"
8
+
9
+ import {GRANTS_FIELD} from "../constants"
10
+
11
+
12
+ export const ResourceSelector = ({field, index, update}) => {
13
+ const {
14
+ formState: {errors},
15
+ setValue,
16
+ } = useFormContext()
17
+
18
+ const fieldKey = `${GRANTS_FIELD}.${index}.resources`
19
+
20
+ const err = _get(errors, fieldKey, null)
21
+
22
+ // const [selectedAll, setSelectedAll] = useState(field.resources?.[0] === "*")
23
+ const [selectedAll, setSelectedAll] = useState(false)
24
+
25
+ const [users, setUsers] = useState([])
26
+ const [selected, setSelected] = useState([])
27
+
28
+ useEffect(() => {
29
+ setUsers([{name: "tmp res 1"}, {name: "tmp res 2"}])
30
+ }, [setUsers])
31
+
32
+ const updateValue = (val) => {
33
+ setValue(fieldKey, val, {
34
+ shouldTouch: true,
35
+ shouldValidate: !!err, // revalidate if there is an error
36
+ })
37
+ }
38
+
39
+ const onChangeSelectedAll = (e) => {
40
+ const {checked} = e.target
41
+ console.log("Ochahge", checked)
42
+ setSelectedAll(checked)
43
+ updateValue(checked ? ["*"] : null)
44
+ }
45
+
46
+ const onSelectResource = (sel) => {
47
+ setSelected(sel)
48
+ updateValue(sel)
49
+ }
50
+
51
+ return (
52
+ <div className="mb-3">
53
+ <div className="h6 fw-bold">On Resources:</div>
54
+ <div>
55
+ All and any new resources in this group:
56
+ <Form.Check
57
+ className="d-block w-100"
58
+ type="switch"
59
+ id={`field-${field.id}-${index}-res-all-switch`}
60
+ label={selectedAll ? "Grant applies to every resource in this group" : "Not applied"}
61
+ // TODO: is this a bug ? why does it need both value and checked
62
+ // this was fixed by using field.id and not index I think
63
+ checked={selectedAll}
64
+ value={selectedAll}
65
+ onChange={onChangeSelectedAll}
66
+ />
67
+ <div className={cx({"text-secondary": selectedAll})}>
68
+ <i>OR:</i> Select Resources:
69
+ </div>
70
+ <Typeahead
71
+ id={`field-${field.id}-${index}-input-role-resources`}
72
+ isInvalid={!!err}
73
+ labelKey="name"
74
+ multiple
75
+ highlightOnlyResult
76
+ disabled={selectedAll}
77
+ onChange={onSelectResource}
78
+ options={users}
79
+ placeholder="Select items..."
80
+ selected={selected}
81
+ />
82
+ {!!err && <p className="text-danger">{err.message}</p>}
83
+ </div>
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,96 @@
1
+ /* @flow */
2
+ import assert from "assert"
3
+ import _get from "lodash/get"
4
+ import {useState, useEffect} from "react"
5
+
6
+ import {useFormContext} from "react-hook-form"
7
+ import {Typeahead} from "react-bootstrap-typeahead"
8
+ import Form from "react-bootstrap/Form"
9
+
10
+ import {GRANTS_FIELD} from "../constants"
11
+
12
+ // import list_users from "rpc!server/users/list_users"
13
+
14
+ // TODO: user selector render avatar next to name
15
+ export const UsersSelector = ({field, index}) => {
16
+ const {
17
+ formState: {errors},
18
+ setValue,
19
+ } = useFormContext()
20
+
21
+ const fieldKey = `${GRANTS_FIELD}.${index}.users`
22
+
23
+ const err = _get(errors, fieldKey, null)
24
+
25
+ const [selectedAll, setSelectedAll] = useState(field.resources?.[0] === "*")
26
+
27
+ const [users, setUsers] = useState([])
28
+ const [selected, setSelected] = useState([])
29
+
30
+ useEffect(() => {
31
+ const load = async() => {
32
+ console.log("LOAD LIST USERS NOT IMPLEMENTED")
33
+ // const res = await list_users()
34
+
35
+ // assert(res.status === "ok")
36
+ // // map user props to get a label, if no name, fallback to email
37
+ // const result = res.users.map((user) => {
38
+ // const name = `${user.first_name} ${user.last_name}`.trim()
39
+ // user.label = name || user.email
40
+ // return user
41
+ // })
42
+ // setUsers(result)
43
+ }
44
+ load()
45
+ }, [setUsers])
46
+
47
+ const updateValue = (val) => {
48
+ setValue(fieldKey, val, {
49
+ shouldTouch: true,
50
+ shouldValidate: !!err, // revalidate if there is an error
51
+ })
52
+ }
53
+
54
+ const onChangeSelectedAll = (e) => {
55
+ const {checked} = e.target
56
+ setSelectedAll(checked)
57
+ updateValue(checked ? ["*"] : null)
58
+ }
59
+
60
+ const onSelectUsers = (sel) => {
61
+ setSelected(sel)
62
+ updateValue(sel.map((s) => s._id))
63
+ }
64
+
65
+ return (
66
+ <div className="mb-2">
67
+ <div className="h6 fw-bold">To Users:</div>
68
+ <div>
69
+ All and any new users in this group:
70
+ <Form.Check
71
+ className="d-block w-100"
72
+ type="switch"
73
+ id={`field-${field.id}-${index}-users-all-switch`}
74
+ label={selectedAll ? "Grant applies to every user in this group" : "Not applied"}
75
+ checked={selectedAll}
76
+ value={selectedAll}
77
+ onChange={onChangeSelectedAll}
78
+ />
79
+ <div className={cx({"text-secondary": selectedAll})}>OR: Select Users:</div>
80
+ <Typeahead
81
+ id={`field-${field.id}-${index}-input-role-users`}
82
+ isInvalid={!!err}
83
+ labelKey={(option) => `${option.label} ${option.email}`}
84
+ multiple
85
+ highlightOnlyResult
86
+ disabled={selectedAll}
87
+ onChange={onSelectUsers}
88
+ options={users}
89
+ placeholder="Select users..."
90
+ selected={selected}
91
+ />
92
+ {!!err && <p className="text-danger">{err.message}</p>}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,26 @@
1
+ @import "helpers";
2
+
3
+ .grant-field-card {
4
+ &:not(:first-of-type) {
5
+ // TODO: what is the way to reuse bootstrap classes here??
6
+ margin-top: $spacer;
7
+ }
8
+
9
+ .grant-field-remove-button {
10
+ position: absolute;
11
+ width: 14px;
12
+ height: 14px;
13
+ right: 0;
14
+ top: 0;
15
+ cursor: pointer;
16
+ color: $gray-500;
17
+
18
+ &:hover {
19
+ color: $gray-700;
20
+ }
21
+
22
+ &:active {
23
+ color: $gray-900;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,16 @@
1
+ export const CheckMark = ({hide = true}) => (
2
+ <svg
3
+ style={{
4
+ marginLeft: -18,
5
+ marginTop: -4,
6
+ marginRight: 3,
7
+ visibility: hide ? "hidden" : undefined,
8
+ }}
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ viewBox="0 0 64 64"
11
+ width={18}
12
+ height={18}
13
+ >
14
+ <path fill="#0d6efd" d="M27 55L6 33 9 29 26 41 55 12 59 16z" />
15
+ </svg>
16
+ )
@@ -0,0 +1,14 @@
1
+ export const CollapseArrow = () => (
2
+ <svg
3
+ style={{marginRight: 4}}
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ viewBox="0 0 30 30"
6
+ width={21}
7
+ height={21}
8
+ >
9
+ <path
10
+ fill="currentColor"
11
+ d="M 14.984375 9 A 1.0001 1.0001 0 0 0 14.292969 9.2929688 L 4.2929688 19.292969 A 1.0001 1.0001 0 1 0 5.7070312 20.707031 L 15 11.414062 L 24.292969 20.707031 A 1.0001 1.0001 0 1 0 25.707031 19.292969 L 15.707031 9.2929688 A 1.0001 1.0001 0 0 0 14.984375 9 z"
12
+ />
13
+ </svg>
14
+ )
@@ -0,0 +1,14 @@
1
+ export const ExpandArrow = () => (
2
+ <svg
3
+ style={{marginRight: 4}}
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ viewBox="0 0 30 30"
6
+ width={21}
7
+ height={21}
8
+ >
9
+ <path
10
+ fill="currentColor"
11
+ d="M 24.990234 8.9863281 A 1.0001 1.0001 0 0 0 24.292969 9.2929688 L 15 18.585938 L 5.7070312 9.2929688 A 1.0001 1.0001 0 0 0 4.9902344 8.9902344 A 1.0001 1.0001 0 0 0 4.2929688 10.707031 L 14.292969 20.707031 A 1.0001 1.0001 0 0 0 15.707031 20.707031 L 25.707031 10.707031 A 1.0001 1.0001 0 0 0 24.990234 8.9863281 z"
12
+ />
13
+ </svg>
14
+ )
@@ -0,0 +1,91 @@
1
+ import {useState} from "react"
2
+ import Collapse from "react-bootstrap/Collapse"
3
+
4
+ import {useFormContext} from "react-hook-form"
5
+
6
+ import {CloseIcon} from "../../../../ui/icons"
7
+ import {View} from "../../../../ui/View"
8
+
9
+ import {OpSelector} from "./OpSelector"
10
+ import {ResourceSelector} from "./ResourceSelector"
11
+ import {UsersSelector} from "./UsersSelector"
12
+
13
+ import {ExpandArrow} from "./icons/ExpandArrow"
14
+ import {CollapseArrow} from "./icons/CollapseArrow"
15
+
16
+ import "./grant-field.scss"
17
+
18
+
19
+ export const GrantField = ({remove, field, index}) => {
20
+ const {getValues} = useFormContext()
21
+
22
+ const {_isEditing} = getValues()
23
+
24
+ const shouldInitiallyBeOpen = () => {
25
+ if (_isEditing && field.ops && field.resources && field.users) {
26
+ return false
27
+ }
28
+ return true
29
+ }
30
+
31
+ const [isCollapseOpen, setIsCollapseOpen] = useState(shouldInitiallyBeOpen())
32
+ // used to remove only after transition is complete
33
+ const [scheduledRemove, setScheduledRemove] = useState(false)
34
+
35
+ const removeField = () => {
36
+ remove(index)
37
+ }
38
+
39
+ const onExited = () => {
40
+ if (scheduledRemove) {
41
+ removeField()
42
+ }
43
+ }
44
+
45
+ const onClickRemove = () => {
46
+ if (isCollapseOpen) {
47
+ setScheduledRemove(true)
48
+ setIsCollapseOpen(false)
49
+ } else {
50
+ removeField()
51
+ }
52
+ }
53
+
54
+ const onClickCollapse = () => setIsCollapseOpen(!isCollapseOpen)
55
+
56
+ return (
57
+ <div className="card grant-field-card p-2">
58
+ {/* Remove Button */}
59
+ <View
60
+ className="grant-field-remove-button mt-3 me-3"
61
+ onClick={onClickRemove}
62
+ tooltip="Remove Grant"
63
+ >
64
+ <CloseIcon size={14} />
65
+ </View>
66
+
67
+ <div className="">
68
+ Allow create, read update delete on hardcoded
69
+ <br />
70
+ grant summary on users Test User, and Another One
71
+ </div>
72
+
73
+ <Collapse appear in={isCollapseOpen} onExited={onExited}>
74
+ <div>
75
+ <OpSelector field={field} index={index} />
76
+ <ResourceSelector field={field} index={index} />
77
+ <UsersSelector field={field} index={index} />
78
+ </div>
79
+ </Collapse>
80
+
81
+ <button
82
+ type="button"
83
+ className="btn btn-link btn-collapse-expand d-flex flex-row align-items-center ps-0"
84
+ onClick={onClickCollapse}
85
+ >
86
+ {isCollapseOpen ? <CollapseArrow /> : <ExpandArrow />}
87
+ {isCollapseOpen ? "Collapse" : "Expand"}
88
+ </button>
89
+ </div>
90
+ )
91
+ }
@@ -0,0 +1,48 @@
1
+ import {useFieldArray, useFormContext} from "react-hook-form"
2
+
3
+ import {GrantField} from "./GrantField"
4
+ import {GRANTS_FIELD} from "./constants"
5
+
6
+
7
+ export const GrantsList = () => {
8
+ // TODO: integrate sortable-hoc list
9
+ // const {fields, append, prepend, remove, swap, update, move, insert} = useFieldArray({
10
+ const {
11
+ formState: {errors},
12
+ } = useFormContext()
13
+ const {fields, append, remove, update} = useFieldArray({name: GRANTS_FIELD})
14
+
15
+ return (
16
+ <>
17
+ <div className="mt-2">
18
+ <label className="h6">Grants</label>
19
+ {fields.map((field, index) => (
20
+ <GrantField
21
+ // IMPORTANT: key must be field id to avoid rerenders
22
+ key={field.id}
23
+ index={index}
24
+ field={field}
25
+ remove={remove}
26
+ update={update}
27
+ />
28
+ ))}
29
+ {fields.length === 0 && (
30
+ <div className="text-secondary">This role doesn't have any Grants</div>
31
+ )}
32
+ {errors.grants?.message && <div className="text-danger">{errors.grants.message}</div>}
33
+ </div>
34
+
35
+ <button
36
+ type="button"
37
+ className="btn btn-light mt-2 px-3"
38
+ onClick={() => {
39
+ append({})
40
+ // trigger()
41
+ }}
42
+ style={{width: "fit-content"}}
43
+ >
44
+ Add {fields.length > 0 && "another"} Grant
45
+ </button>
46
+ </>
47
+ )
48
+ }
@@ -0,0 +1,134 @@
1
+ /* eslint no-unused-vars: "warn" */
2
+ import assert from "assert"
3
+ import {useState} from "react"
4
+ import {useForm, FormProvider} from "react-hook-form"
5
+ import Collapse from "react-bootstrap/Collapse"
6
+
7
+ import {SubmitButton} from "@rpcbase/client/form"
8
+
9
+ import {CloseIcon} from "../../../ui/icons/Close"
10
+ import {View} from "../../../ui/View"
11
+
12
+ import {resolver} from "./resolver"
13
+ import {GrantsList} from "./GrantsList"
14
+
15
+ // import create_role from "rpc!server/access-control/create_role"
16
+
17
+ import "./role-form.scss"
18
+
19
+
20
+ export const RoleForm = ({onDone}) => {
21
+ const [isLoading, setIsLoading] = useState(false)
22
+
23
+ // react hook form submit on edit
24
+ // https://stackoverflow.com/a/70119332
25
+ const form = useForm({
26
+ defaultValues: {
27
+ name: "",
28
+ description: "",
29
+ grants: [{}],
30
+ },
31
+ resolver,
32
+ reValidateMode: "onChange",
33
+ })
34
+
35
+ const {
36
+ register,
37
+ handleSubmit,
38
+ // watch,
39
+ getValues,
40
+ formState: {errors},
41
+ } = form
42
+
43
+ const onSubmit = async(data) => {
44
+ // console.log("submit", group_id, data)
45
+ setIsLoading(true)
46
+ // const res = await create_role({...data})
47
+ // assert(res.status === "ok")
48
+ // TODO: error handling here
49
+ setIsLoading(false)
50
+ onDone()
51
+ }
52
+
53
+ // TODO: ask for user confirmation is form is touched
54
+ // TODO: should we save touched fields in url hashState?
55
+ const onClickCancel = () => {
56
+ onDone()
57
+ }
58
+
59
+ const onClickRemove = () => {
60
+ onDone()
61
+ }
62
+
63
+ // useEffect(() => {
64
+ // console.log("le vals", getValues())
65
+ // }, [formState])
66
+
67
+ return (
68
+ <FormProvider {...form}>
69
+ <div className="mx-3" style={{position: "relative"}}>
70
+ <Collapse appear in={true}>
71
+ <form className="mt-2 mb-1 card" onSubmit={handleSubmit(onSubmit)}>
72
+ <div className="p-2">
73
+ <div className="h6 fw-bold">Create New Role:</div>
74
+ <div className="mt-2">
75
+ <label htmlFor="input-role-name" className="h6">
76
+ Name
77
+ </label>
78
+ <input
79
+ type="text"
80
+ className={cx("form-control", {"is-invalid": !!errors.name})}
81
+ id="input-role-name"
82
+ {...register("name", {required: true})}
83
+ placeholder="Role Name"
84
+ />
85
+ {errors.name && <div className="text-danger">{errors.name.message}</div>}
86
+ </div>
87
+
88
+ <div className="mt-2">
89
+ <label htmlFor="input-view-name" className="h6">
90
+ Description
91
+ </label>
92
+ <input
93
+ type="text"
94
+ className={cx("form-control", {"is-invalid": !!errors.description})}
95
+ id="input-role-description"
96
+ {...register("description", {required: true})}
97
+ placeholder="Description"
98
+ />
99
+ {errors.description && (
100
+ <div className="text-danger">{errors.description.message}</div>
101
+ )}
102
+ </div>
103
+
104
+ {/* Grants list */}
105
+ <GrantsList />
106
+ </div>
107
+
108
+ <div className="card-footer">
109
+ <SubmitButton className="me-2" isLoading={isLoading}>
110
+ {isLoading ? "Saving Role..." : "Save Role"}
111
+ </SubmitButton>
112
+
113
+ <button
114
+ type="button"
115
+ className="btn btn-link link-secondary ps-0"
116
+ onClick={onClickCancel}
117
+ disabled={isLoading}
118
+ >
119
+ Cancel
120
+ </button>
121
+ </div>
122
+ </form>
123
+ </Collapse>
124
+ <View
125
+ className="role-remove-button mt-3 me-3"
126
+ onClick={onClickRemove}
127
+ tooltip="Remove Role"
128
+ >
129
+ <CloseIcon size={14} />
130
+ </View>
131
+ </div>
132
+ </FormProvider>
133
+ )
134
+ }