Skip to content

Commit

Permalink
UI: Change capabilities in user or group view (#770)
Browse files Browse the repository at this point in the history
  • Loading branch information
postrowinski committed Apr 20, 2023
1 parent 1a8b5f8 commit cc97fb2
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 26 deletions.
5 changes: 1 addition & 4 deletions mwdb/web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,7 @@ function AppRoutes() {
element={<ProfileGroupMembers />}
/>
<Route path="groups" element={<ProfileGroups />} />
<Route
path="capabilities"
element={<ProfileCapabilities />}
/>
<Route path="capabilities" element={<UserCapabilities />} />
<Route path="api-keys" element={<ProfileAPIKeys />} />
<Route
path="reset-password"
Expand Down
1 change: 1 addition & 0 deletions mwdb/web/src/commons/hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useCheckCapabilities";
14 changes: 14 additions & 0 deletions mwdb/web/src/commons/hooks/useCheckCapabilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useContext } from "react";
import { AuthContext } from "@mwdb-web/commons/auth";

export function useCheckCapabilities() {
const { user } = useContext(AuthContext);

function userHasCapabilities(cap) {
return user.capabilities.includes(cap);
}

return {
userHasCapabilities,
};
}
31 changes: 29 additions & 2 deletions mwdb/web/src/components/Profile/ProfileView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { api } from "@mwdb-web/commons/api";
import { AuthContext } from "@mwdb-web/commons/auth";
import { ConfigContext } from "@mwdb-web/commons/config";
import { View, useViewAlert } from "@mwdb-web/commons/ui";
import DeleteCapabilityModal from "../Settings/Views/DeleteCapabilityModal";

function ProfileNav() {
const config = useContext(ConfigContext);
Expand Down Expand Up @@ -46,14 +47,28 @@ function ProfileNav() {

export default function ProfileView() {
const auth = useContext(AuthContext);
const { redirectToAlert } = useViewAlert();
const { redirectToAlert, setAlert } = useViewAlert();
const user = useParams().user || auth.user.login;
const [profile, setProfile] = useState();
const [capabilitiesToDelete, setCapabilitiesToDelete] = useState("");

useEffect(() => {
getProfile();
}, [user]);

async function changeCapabilities(capability, callback) {
try {
const capabilities = profile.capabilities.filter(
(item) => item !== capability
);
await api.updateGroup(profile.login, { capabilities });
getProfile();
callback();
} catch (error) {
setAlert({ error });
}
}

async function getProfile() {
try {
const response = await api.getUserProfile(user);
Expand All @@ -75,9 +90,21 @@ export default function ProfileView() {
<ProfileNav />
</div>
<div className="col-sm-8">
<Outlet context={{ profile, getProfile }} />
<Outlet
context={{
profile,
getUser: getProfile,
setCapabilitiesToDelete,
}}
/>
</div>
</div>
<DeleteCapabilityModal
changeCapabilities={changeCapabilities}
capabilitiesToDelete={capabilitiesToDelete}
setCapabilitiesToDelete={setCapabilitiesToDelete}
successMessage={`Capabilities for ${user} successfully changed`}
/>
</View>
);
}
208 changes: 199 additions & 9 deletions mwdb/web/src/components/Profile/Views/ProfileCapabilities.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
import React from "react";
import React, { useState, useCallback, useEffect } from "react";
import { useOutletContext } from "react-router-dom";

import { capabilitiesList } from "@mwdb-web/commons/auth";
import { GroupBadge } from "@mwdb-web/commons/ui";
import { Link } from "react-router-dom";
import { faTimes, faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { find, isNil, isEmpty } from "lodash";
import { api } from "@mwdb-web/commons/api";
import { capabilitiesList, Capability } from "@mwdb-web/commons/auth";
import {
GroupBadge,
BootstrapSelect,
ConfirmationModal,
useViewAlert,
} from "@mwdb-web/commons/ui";
import { useCheckCapabilities } from "@mwdb-web/commons/hooks";

function CapabilitiesTable({ profile }) {
const { userHasCapabilities } = useCheckCapabilities();
const { setCapabilitiesToDelete } = useOutletContext();

function isUserDeleteButtonRender(cap) {
const userCap = find(profile.groups, { name: profile.login });
if (isNil(userCap)) {
return false;
}
return userCap.capabilities.includes(cap);
}

function isDeleteButtonRender(cap) {
return !isNil(profile.login) ? isUserDeleteButtonRender(cap) : true;
}

if (!profile.capabilities) return [];
return (
<table className="table table-bordered wrap-table">
<tbody>
{profile.capabilities.sort().map((cap) => (
<tr>
<th>
<tr key={cap}>
{userHasCapabilities(Capability.manageUsers) && (
<td className="col-auto">
{isDeleteButtonRender(cap) && (
<Link
to={"#"}
onClick={(ev) => {
ev.preventDefault();
setCapabilitiesToDelete(cap);
}}
>
<FontAwesomeIcon icon={faTimes} />
</Link>
)}
</td>
)}
<td>
<span className="badge badge-success">{cap}</span>
</th>
</td>
<td>
<div>
{capabilitiesList[cap] || "(no description)"}
Expand All @@ -28,8 +68,9 @@ function CapabilitiesTable({ profile }) {
.filter((group) =>
group.capabilities.includes(cap)
)
.map((group) => (
.map((group, index) => (
<GroupBadge
key={index}
group={group}
clickable
/>
Expand All @@ -45,19 +86,168 @@ function CapabilitiesTable({ profile }) {
);
}

export default function ProfileCapabilities({ profile }) {
function CapabilitiesSelect({ profile, getData }) {
const { setAlert } = useViewAlert();

const [chosenCapabilities, setChosenCapabilities] = useState([]);
const [correctCapabilities, setCorrectCapabilities] = useState([]);
const [isOpen, setIsOpen] = useState(false);

const isAccount = !isNil(profile.groups);
const group = isAccount ? profile.login : profile.name;
const capabilities = Object.keys(capabilitiesList);
const changedCaps = capabilities.filter(
(cap) =>
chosenCapabilities.includes(cap) !==
correctCapabilities.includes(cap)
);

const selectHandler = useCallback(
(e, clickedIndex, isSelected) => {
const selectedCaps = chosenCapabilities || [];
if (isSelected)
setChosenCapabilities(
selectedCaps.concat(capabilities[clickedIndex])
);
else
setChosenCapabilities(
selectedCaps.filter(
(cap) => cap !== capabilities[clickedIndex]
)
);
},
[chosenCapabilities, capabilities]
);

async function changeCapabilities() {
try {
await api.updateGroup(group, { capabilities: chosenCapabilities });
getData();
setIsOpen(false);
setAlert({
success: `Capabilities for ${group} successfully changed`,
});
} catch (error) {
setAlert({ error });
}
}

function dismissChanges() {
setChosenCapabilities(correctCapabilities);
}

useEffect(() => {
if (!isEmpty(profile)) {
if (isAccount) {
const foundGroup = find(profile.groups, {
name: profile.login,
});
if (!isNil(foundGroup)) {
const newCapabilities = foundGroup.capabilities;
setChosenCapabilities(newCapabilities);
setCorrectCapabilities(newCapabilities);
}
} else {
const newCapabilities = profile.capabilities;
setChosenCapabilities(newCapabilities);
setCorrectCapabilities(newCapabilities);
}
}
}, [profile]);

return (
<div className="mb-3">
<div className="row">
<BootstrapSelect
data-multiple-separator={""}
data-live-search="true"
noneSelectedText={"No capabilities selected"}
className={"col-lg-9"}
multiple
onChange={selectHandler}
>
{capabilities.map((cap) => {
const selectedCaps = chosenCapabilities || [];
const selected = selectedCaps.includes(cap);
const changed =
chosenCapabilities.includes(cap) !==
correctCapabilities.includes(cap);

return (
<option
key={cap}
data-content={`
${changed ? "*" : ""}
<span class='badge badge-success'>${cap}</span>
<small class="text-muted">${cap}</small>
`}
value={cap}
selected={selected}
>
{cap}
</option>
);
})}
</BootstrapSelect>
<div
className="col-lg-3 justify-content-between"
style={{ display: "flex" }}
>
<button
className="btn btn-outline-success"
disabled={changedCaps.length === 0}
onClick={() => setIsOpen(true)}
>
<FontAwesomeIcon icon={faSave} /> Apply
</button>
<button
className="btn btn-outline-danger"
disabled={changedCaps.length === 0}
onClick={() => dismissChanges()}
>
<FontAwesomeIcon icon={faTimes} /> Dismiss
</button>
</div>
</div>
{changedCaps.length > 0 && (
<div>
<small>
* There are pending changes. Click Apply if you want to
commit them or Dismiss otherwise.
</small>
</div>
)}
<ConfirmationModal
buttonStyle="badge-success"
confirmText="Yes"
message={`Are you sure you want to change '${group}' capabilities?`}
isOpen={isOpen}
onRequestClose={() => setIsOpen(false)}
onConfirm={changeCapabilities}
/>
</div>
);
}

export default function ProfileCapabilities({ profile, getData }) {
// Component is reused by Settings
const outletContext = useOutletContext();
const { userHasCapabilities } = useCheckCapabilities();

if (profile === undefined) {
profile = outletContext.profile;
}

return (
<div className="container">
<h2>Capabilities</h2>
<p className="lead">
Here is the list of {profile.groups ? "account" : "group"}{" "}
superpowers:
</p>
{userHasCapabilities(Capability.manageUsers) && (
<CapabilitiesSelect profile={profile} getData={getData} />
)}
<CapabilitiesTable profile={profile} />
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions mwdb/web/src/components/Profile/Views/ProfileGroup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ export default function ProfileGroup() {
.sort((userA, userB) =>
userA.localeCompare(userB)
)
.map((login) => (
.map((login, index) => (
<GroupBadge
key={index}
group={{
name: login,
private: true,
Expand All @@ -96,8 +97,9 @@ export default function ProfileGroup() {
.sort((userA, userB) =>
userA.localeCompare(userB)
)
.map((login) => (
.map((login, index) => (
<GroupBadge
key={index}
group={{
name: login,
private: true,
Expand Down
2 changes: 1 addition & 1 deletion mwdb/web/src/components/Settings/Views/AttributeView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function AttributeView() {

function BreadcrumbItems({ elements = [] }) {
return [
<li key={"attribute-details"} className="breadcrumb-item">
<li className="breadcrumb-item" key="details">
<strong>Attribute details: </strong>
{elements.length > 0 ? (
<Link to={`/settings/attribute/${attribute.key}`}>
Expand Down

0 comments on commit cc97fb2

Please sign in to comment.