client/app/components/PermissionsEditorDialog/index.jsx (175 lines of code) (raw):
import React, { useState, useEffect, useCallback } from "react";
import { axios } from "@/services/axios";
import PropTypes from "prop-types";
import { each, debounce, get, find } from "lodash";
import Button from "antd/lib/button";
import List from "antd/lib/list";
import Modal from "antd/lib/modal";
import Select from "antd/lib/select";
import Tag from "antd/lib/tag";
import Tooltip from "antd/lib/tooltip";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { toHuman } from "@/lib/utils";
import HelpTrigger from "@/components/HelpTrigger";
import { UserPreviewCard } from "@/components/PreviewCard";
import notification from "@/services/notification";
import User from "@/services/user";
import "./index.less";
const { Option } = Select;
const DEBOUNCE_SEARCH_DURATION = 200;
function useGrantees(url) {
const loadGrantees = useCallback(
() =>
axios.get(url).then(data => {
const resultGrantees = [];
each(data, (grantees, accessType) => {
grantees.forEach(grantee => {
grantee.accessType = toHuman(accessType);
resultGrantees.push(grantee);
});
});
return resultGrantees;
}),
[url]
);
const addPermission = useCallback(
(userId, accessType = "modify") =>
axios
.post(url, { access_type: accessType, user_id: userId })
.catch(() => notification.error("Could not grant permission to the user")),
[url]
);
const removePermission = useCallback(
(userId, accessType = "modify") =>
axios
.delete(url, { data: { access_type: accessType, user_id: userId } })
.catch(() => notification.error("Could not remove permission from the user")),
[url]
);
return { loadGrantees, addPermission, removePermission };
}
const searchUsers = searchTerm =>
User.query({ q: searchTerm })
.then(({ results }) => results)
.catch(() => []);
function PermissionsEditorDialogHeader({ context }) {
return (
<>
Manage Permissions
<div className="modal-header-desc">
{`Editing this ${context} is enabled for the users in this list and for admins. `}
<HelpTrigger type="MANAGE_PERMISSIONS" />
</div>
</>
);
}
PermissionsEditorDialogHeader.propTypes = { context: PropTypes.oneOf(["query", "dashboard"]) };
PermissionsEditorDialogHeader.defaultProps = { context: "query" };
function UserSelect({ onSelect, shouldShowUser }) {
const [loadingUsers, setLoadingUsers] = useState(true);
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchUsers = useCallback(
debounce(
search =>
searchUsers(search)
.then(setUsers)
.finally(() => setLoadingUsers(false)),
DEBOUNCE_SEARCH_DURATION
),
[]
);
useEffect(() => {
setLoadingUsers(true);
debouncedSearchUsers(searchTerm);
}, [debouncedSearchUsers, searchTerm]);
return (
<Select
className="w-100 m-b-10"
placeholder="Add users..."
showSearch
onSearch={setSearchTerm}
suffixIcon={loadingUsers ? <i className="fa fa-spinner fa-pulse" /> : <i className="fa fa-search" />}
filterOption={false}
notFoundContent={null}
value={undefined}
getPopupContainer={trigger => trigger.parentNode}
onSelect={onSelect}>
{users.filter(shouldShowUser).map(user => (
<Option key={user.id} value={user.id}>
<UserPreviewCard user={user} />
</Option>
))}
</Select>
);
}
UserSelect.propTypes = {
onSelect: PropTypes.func,
shouldShowUser: PropTypes.func,
};
UserSelect.defaultProps = { onSelect: () => {}, shouldShowUser: () => true };
function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
const [loadingGrantees, setLoadingGrantees] = useState(true);
const [grantees, setGrantees] = useState([]);
const { loadGrantees, addPermission, removePermission } = useGrantees(aclUrl);
const loadUsersWithPermissions = useCallback(() => {
setLoadingGrantees(true);
loadGrantees()
.then(setGrantees)
.catch(() => notification.error("Failed to load grantees list"))
.finally(() => setLoadingGrantees(false));
}, [loadGrantees]);
const userHasPermission = useCallback(
user => user.id === author.id || !!get(find(grantees, { id: user.id }), "accessType"),
[author.id, grantees]
);
useEffect(() => {
loadUsersWithPermissions();
}, [aclUrl, loadUsersWithPermissions]);
return (
<Modal
{...dialog.props}
className="permissions-editor-dialog"
title={<PermissionsEditorDialogHeader context={context} />}
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<UserSelect
onSelect={userId => addPermission(userId).then(loadUsersWithPermissions)}
shouldShowUser={user => !userHasPermission(user)}
/>
<div className="d-flex align-items-center m-t-5">
<h5 className="flex-fill">Users with permissions</h5>
{loadingGrantees && <i className="fa fa-spinner fa-pulse" />}
</div>
<div className="scrollbox p-5" style={{ maxHeight: "40vh" }}>
<List
size="small"
dataSource={[author, ...grantees]}
renderItem={user => (
<List.Item>
<UserPreviewCard key={user.id} user={user}>
{user.id === author.id ? (
<Tag className="m-0">Author</Tag>
) : (
<Tooltip title="Remove user permissions">
<i
className="fa fa-remove clickable"
onClick={() => removePermission(user.id).then(loadUsersWithPermissions)}
/>
</Tooltip>
)}
</UserPreviewCard>
</List.Item>
)}
/>
</div>
</Modal>
);
}
PermissionsEditorDialog.propTypes = {
dialog: DialogPropType.isRequired,
author: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
context: PropTypes.oneOf(["query", "dashboard"]),
aclUrl: PropTypes.string.isRequired,
};
PermissionsEditorDialog.defaultProps = { context: "query" };
export default wrapDialog(PermissionsEditorDialog);