mirror of
https://github.com/getredash/redash.git
synced 2026-03-21 16:00:09 -04:00
* Snapshot: 24.07.0-dev * Snapshot: 24.08.0-dev * Snapshot: 24.09.0-dev * Snapshot: 24.10.0-dev * Snapshot: 24.11.0-dev * Snapshot: 24.12.0-dev * Snapshot: 25.01.0-dev * Snapshot: 25.02.0-dev * Snapshot: 25.03.0-dev * Snapshot: 25.04.0-dev * Upgrade Node.js version to 24 in Dockerfile and .nvmrc; update package.json engine constraints * Update major dependencies * Switch from yarn to pnpm * Switch from yarn to pnpm: ci * Update Python version to 3.13 in CI workflow * Refactor Netlify build command to remove pnpm installation step * Update ESLint configuration for improved compatibility and disable specific rules * Restyled by prettier * Add typeRoots and types to tsconfig for improved type definitions * Update Dockerfile.cypress to use Node 24 and streamline installation steps * Fixed tests * Restyled by prettier * Update Jest snapshot comments to point to the official documentation URL * viz-lib: refactor test setup and update snapshots for consistency * Add babel-jest as a dev dependency for improved testing support * Add virtual prop to visualization type selector for improved functionality * Remove CJS/ESM compatibility shim for color-rgba * Restyled by prettier * Enable ESLintPlugin conditionally based on production environment * Import d3 library in d3box.ts * Fix pip install command in CI workflow to use python -m * Replace d3 import with global declaration for compatibility with d3 v3 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Restyled.io <commits@restyled.io>
217 lines
6.9 KiB
JavaScript
217 lines
6.9 KiB
JavaScript
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 "@/components/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 PlainButton from "@/components/PlainButton";
|
|
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("");
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
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 ? (
|
|
<span role="status" aria-live="polite" aria-relevant="additions removals">
|
|
<i className="fa fa-spinner fa-pulse" aria-hidden="true" />
|
|
<span className="sr-only">Loading...</span>
|
|
</span>
|
|
) : (
|
|
<i className="fa fa-search" aria-hidden="true" />
|
|
)
|
|
}
|
|
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 && (
|
|
<span role="status" aria-live="polite" aria-relevant="additions removals">
|
|
<i className="fa fa-spinner fa-pulse" aria-hidden="true" />
|
|
<span className="sr-only">Loading...</span>
|
|
</span>
|
|
)}
|
|
</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">
|
|
<PlainButton
|
|
aria-label="Remove permissions"
|
|
onClick={() => removePermission(user.id).then(loadUsersWithPermissions)}
|
|
>
|
|
<i className="fa fa-remove clickable" aria-hidden="true" />
|
|
</PlainButton>
|
|
</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);
|