From cc1f3a9b694ad80b73107ebc1344ba86b17b891c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Ostrowi=C5=84ski?= Date: Mon, 29 May 2023 20:43:25 +0200 Subject: [PATCH] TS: add types to components/Profile (#813) * init commit * rewrite all components/Profile files --- .../hooks/useCheckCapabilities.test.tsx | 3 +- mwdb/web/src/commons/api/index.tsx | 40 ++- mwdb/web/src/commons/auth/context.tsx | 2 +- mwdb/web/src/commons/auth/provider.tsx | 3 +- mwdb/web/src/commons/config/context.tsx | 2 +- mwdb/web/src/commons/config/provider.tsx | 3 +- mwdb/web/src/commons/ui/DateString.tsx | 5 +- mwdb/web/src/commons/ui/ObjectTab.tsx | 2 +- mwdb/web/src/commons/ui/ShowIf.tsx | 4 +- .../{ProfileView.jsx => ProfileView.tsx} | 24 +- .../Profile/Views/ProfileAPIKeys.jsx | 230 ----------------- .../Profile/Views/ProfileAPIKeys.tsx | 212 ++++++++++++++++ .../Profile/Views/ProfileCapabilities.jsx | 233 ------------------ .../Profile/Views/ProfileCapabilities.tsx | 35 +++ ...{ProfileDetails.jsx => ProfileDetails.tsx} | 33 ++- .../{ProfileGroup.jsx => ProfileGroup.tsx} | 87 ++++--- .../Profile/Views/ProfileGroupMembers.jsx | 150 ----------- .../Profile/Views/ProfileGroupMembers.tsx | 89 +++++++ .../{ProfileGroups.jsx => ProfileGroups.tsx} | 8 +- .../{ProfileOAuth.jsx => ProfileOAuth.tsx} | 45 ++-- ...tPassword.jsx => ProfileResetPassword.tsx} | 35 +-- .../Profile/common/CapabilitiesSelect.tsx | 144 +++++++++++ .../Profile/common/CapabilitiesTable.tsx | 101 ++++++++ .../Profile/common/KeyNameModal.tsx | 51 ++++ .../Profile/common/ProfileGroupItems.tsx | 80 ++++++ .../components/Profile/common/ProfileItem.tsx | 21 ++ .../components/ShowObject/Views/ObjectBox.tsx | 2 +- mwdb/web/src/types/api.ts | 29 ++- mwdb/web/src/types/context.ts | 35 +++ mwdb/web/src/types/props.ts | 6 +- mwdb/web/src/types/types.ts | 32 +-- 31 files changed, 957 insertions(+), 789 deletions(-) rename mwdb/web/src/components/Profile/{ProfileView.jsx => ProfileView.tsx} (83%) delete mode 100644 mwdb/web/src/components/Profile/Views/ProfileAPIKeys.jsx create mode 100644 mwdb/web/src/components/Profile/Views/ProfileAPIKeys.tsx delete mode 100644 mwdb/web/src/components/Profile/Views/ProfileCapabilities.jsx create mode 100644 mwdb/web/src/components/Profile/Views/ProfileCapabilities.tsx rename mwdb/web/src/components/Profile/Views/{ProfileDetails.jsx => ProfileDetails.tsx} (80%) rename mwdb/web/src/components/Profile/Views/{ProfileGroup.jsx => ProfileGroup.tsx} (66%) delete mode 100644 mwdb/web/src/components/Profile/Views/ProfileGroupMembers.jsx create mode 100644 mwdb/web/src/components/Profile/Views/ProfileGroupMembers.tsx rename mwdb/web/src/components/Profile/Views/{ProfileGroups.jsx => ProfileGroups.tsx} (92%) rename mwdb/web/src/components/Profile/Views/{ProfileOAuth.jsx => ProfileOAuth.tsx} (79%) rename mwdb/web/src/components/Profile/Views/{ProfileResetPassword.jsx => ProfileResetPassword.tsx} (61%) create mode 100644 mwdb/web/src/components/Profile/common/CapabilitiesSelect.tsx create mode 100644 mwdb/web/src/components/Profile/common/CapabilitiesTable.tsx create mode 100644 mwdb/web/src/components/Profile/common/KeyNameModal.tsx create mode 100644 mwdb/web/src/components/Profile/common/ProfileGroupItems.tsx create mode 100644 mwdb/web/src/components/Profile/common/ProfileItem.tsx create mode 100644 mwdb/web/src/types/context.ts diff --git a/mwdb/web/src/__tests__/hooks/useCheckCapabilities.test.tsx b/mwdb/web/src/__tests__/hooks/useCheckCapabilities.test.tsx index 0de94a0ff..53bb0b180 100644 --- a/mwdb/web/src/__tests__/hooks/useCheckCapabilities.test.tsx +++ b/mwdb/web/src/__tests__/hooks/useCheckCapabilities.test.tsx @@ -1,8 +1,9 @@ import { renderHook } from "@testing-library/react"; import { useCheckCapabilities } from "@mwdb-web/commons/hooks"; -import { AuthContextValues, Capability } from "@mwdb-web/types/types"; +import { Capability } from "@mwdb-web/types/types"; import { AuthContext } from "@mwdb-web/commons/auth"; import { AuthProviderProps } from "@mwdb-web/types/props"; +import { AuthContextValues } from "@mwdb-web/types/context"; describe("useCheckCapabilities", () => { const authContextValue = { diff --git a/mwdb/web/src/commons/api/index.tsx b/mwdb/web/src/commons/api/index.tsx index 16a7f9711..6d5dffad8 100644 --- a/mwdb/web/src/commons/api/index.tsx +++ b/mwdb/web/src/commons/api/index.tsx @@ -56,6 +56,12 @@ import { GetUserProfileResponse, GetUserResponse, GetUsersResponse, + OauthGetIdentitiesResponse, + OauthGetLogoutLinkResponse, + OauthGetProvidersResponse, + OauthGetSingleProviderResponse, + OauthRemoveSingleProviderResponse, + OauthUpdateSingleProviderResponse, PullObjectRemoteResponse, PushObjectRemoteResponse, RegisterGroupResponse, @@ -87,7 +93,8 @@ import { UploadFileResponse, UserRequestPasswordChangeResponse, } from "@mwdb-web/types/api"; -import { Attribute, ObjectType } from "@mwdb-web/types/types"; +import { Attribute, Capability, ObjectType } from "@mwdb-web/types/types"; +import { APIProviderProps } from "@mwdb-web/types/props"; function getApiForEnvironment() { // Default API endpoint @@ -203,27 +210,34 @@ function oauthRegisterProvider( }); } -function oauthGetProviders() { +function oauthGetProviders(): OauthGetProvidersResponse { return axios.get("/oauth"); } -function oauthGetSingleProvider(provider_name: string) { +function oauthGetSingleProvider( + provider_name: string +): OauthGetSingleProviderResponse { return axios.get(`/oauth/${provider_name}`); } -function oauthUpdateSingleProvider(name: string, value: string) { +function oauthUpdateSingleProvider( + name: string, + value: string +): OauthUpdateSingleProviderResponse { return axios.put(`/oauth/${name}`, value); } -function oauthRemoveSingleProvider(name: string) { +function oauthRemoveSingleProvider( + name: string +): OauthRemoveSingleProviderResponse { return axios.delete(`/oauth/${name}`); } -function oauthGetIdentities() { +function oauthGetIdentities(): OauthGetIdentitiesResponse { return axios.get("/oauth/identities"); } -function oauthGetLogoutLink(provider: string) { +function oauthGetLogoutLink(provider: string): OauthGetLogoutLinkResponse { return axios.get(`/oauth/${provider}/logout`); } @@ -231,7 +245,7 @@ function apiKeyAdd(login: string, name: string): ApiKeyAddResponse { return axios.post(`/user/${login}/api_key`, { name }); } -function apiKeyRemove(key_id: number): ApiKeyRemoveResponse { +function apiKeyRemove(key_id: number | string): ApiKeyRemoveResponse { return axios.delete(`/api_key/${key_id}`); } @@ -385,7 +399,10 @@ function registerGroup(name: string): RegisterGroupResponse { return axios.post(`/group/${name}`, { name }); } -function updateGroup(name: string, value: string): UpdateGroupResponse { +function updateGroup( + name: string, + value: { capabilities: Capability[] } +): UpdateGroupResponse { return axios.put(`/group/${name}`, value); } @@ -871,10 +888,7 @@ export const api = { enableSharing3rdParty, }; -type APIProviderProps = { - children: React.ReactNode; -}; - +// TODO: api context is not needed, remove it when all components will rewrite to TypeScript export const APIContext = React.createContext({}); export function APIProvider(props: APIProviderProps) { return ( diff --git a/mwdb/web/src/commons/auth/context.tsx b/mwdb/web/src/commons/auth/context.tsx index 622fe32e3..8486ecad0 100644 --- a/mwdb/web/src/commons/auth/context.tsx +++ b/mwdb/web/src/commons/auth/context.tsx @@ -1,5 +1,5 @@ -import { AuthContextValues } from "@mwdb-web/types/types"; import React from "react"; +import { AuthContextValues } from "@mwdb-web/types/context"; export const AuthContext = React.createContext( {} as AuthContextValues diff --git a/mwdb/web/src/commons/auth/provider.tsx b/mwdb/web/src/commons/auth/provider.tsx index 302ef886f..ac0b538e3 100644 --- a/mwdb/web/src/commons/auth/provider.tsx +++ b/mwdb/web/src/commons/auth/provider.tsx @@ -5,8 +5,9 @@ import { api } from "../api"; import { omit, isEqual, isNil } from "lodash"; import { AuthContext } from "./context"; -import { AuthContextValues, Capability, User } from "@mwdb-web/types/types"; +import { Capability, User } from "@mwdb-web/types/types"; import { AuthProviderProps } from "@mwdb-web/types/props"; +import { AuthContextValues } from "@mwdb-web/types/context"; export const localStorageAuthKey = "user"; diff --git a/mwdb/web/src/commons/config/context.tsx b/mwdb/web/src/commons/config/context.tsx index a747a2347..eb7939f3a 100644 --- a/mwdb/web/src/commons/config/context.tsx +++ b/mwdb/web/src/commons/config/context.tsx @@ -1,4 +1,4 @@ -import { ConfigContextValues } from "@mwdb-web/types/types"; +import { ConfigContextValues } from "@mwdb-web/types/context"; import React from "react"; export const ConfigContext = React.createContext( diff --git a/mwdb/web/src/commons/config/provider.tsx b/mwdb/web/src/commons/config/provider.tsx index d39349356..d5ec1a2b7 100644 --- a/mwdb/web/src/commons/config/provider.tsx +++ b/mwdb/web/src/commons/config/provider.tsx @@ -10,7 +10,8 @@ import { isEqual } from "lodash"; import { api } from "../api"; import { ConfigContext } from "./context"; import { AuthContext } from "../auth"; -import { ConfigContextValues, ServerInfo, User } from "@mwdb-web/types/types"; +import { ServerInfo, User } from "@mwdb-web/types/types"; +import { ConfigContextValues } from "@mwdb-web/types/context"; const configUpdate = Symbol("configUpdate"); const configError = Symbol("configError"); diff --git a/mwdb/web/src/commons/ui/DateString.tsx b/mwdb/web/src/commons/ui/DateString.tsx index 2fbfc5942..6e9e2b3ae 100644 --- a/mwdb/web/src/commons/ui/DateString.tsx +++ b/mwdb/web/src/commons/ui/DateString.tsx @@ -1,8 +1,11 @@ type Props = { - date: string; + date?: string | Date; }; export default function DateString(props: Props) { + if (!props.date) { + return <>; + } const date = props.date; const d = new Date(date); return {date != null ? d.toUTCString() : "(never)"}; diff --git a/mwdb/web/src/commons/ui/ObjectTab.tsx b/mwdb/web/src/commons/ui/ObjectTab.tsx index 55a68fc10..c00563ec4 100644 --- a/mwdb/web/src/commons/ui/ObjectTab.tsx +++ b/mwdb/web/src/commons/ui/ObjectTab.tsx @@ -4,7 +4,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { capitalize } from "../helpers"; -import { TabContextValues } from "@mwdb-web/types/types"; +import { TabContextValues } from "@mwdb-web/types/context"; export const TabContext = React.createContext( {} as TabContextValues diff --git a/mwdb/web/src/commons/ui/ShowIf.tsx b/mwdb/web/src/commons/ui/ShowIf.tsx index a2af10d01..ee6a91be1 100644 --- a/mwdb/web/src/commons/ui/ShowIf.tsx +++ b/mwdb/web/src/commons/ui/ShowIf.tsx @@ -1,8 +1,8 @@ type Props = { condition: boolean; - children: React.ReactNode; + children: JSX.Element; }; export function ShowIf({ condition, children }: Props) { - return condition ? children : []; + return condition ? children : <>; } diff --git a/mwdb/web/src/components/Profile/ProfileView.jsx b/mwdb/web/src/components/Profile/ProfileView.tsx similarity index 83% rename from mwdb/web/src/components/Profile/ProfileView.jsx rename to mwdb/web/src/components/Profile/ProfileView.tsx index b497c4977..e73bc0942 100644 --- a/mwdb/web/src/components/Profile/ProfileView.jsx +++ b/mwdb/web/src/components/Profile/ProfileView.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { NavLink, useParams, Outlet } from "react-router-dom"; import { faUserCog } from "@fortawesome/free-solid-svg-icons"; @@ -10,6 +10,7 @@ import { ConfigContext } from "@mwdb-web/commons/config"; import { View } from "@mwdb-web/commons/ui"; import { useViewAlert } from "@mwdb-web/commons/hooks"; import DeleteCapabilityModal from "../Settings/Views/DeleteCapabilityModal"; +import { Capability, User } from "@mwdb-web/types/types"; function ProfileNav() { const config = useContext(ConfigContext); @@ -32,12 +33,10 @@ function ProfileNav() { API keys - {config.config["is_oidc_enabled"] ? ( + {config.config["is_oidc_enabled"] && ( OpenID Connect - ) : ( - [] )}
@@ -48,15 +47,18 @@ function ProfileNav() { export default function ProfileView() { const auth = useContext(AuthContext); const { redirectToAlert, setAlert } = useViewAlert(); - const user = useParams().user || auth.user.login; - const [profile, setProfile] = useState({}); + const userLogin = useParams().user || auth.user.login; + const [profile, setProfile] = useState({} as User); const [capabilitiesToDelete, setCapabilitiesToDelete] = useState(""); useEffect(() => { getProfile(); - }, [user]); + }, [userLogin]); - async function changeCapabilities(capability, callback) { + async function changeCapabilities( + capability: Capability, + callback: Function + ) { try { const capabilities = profile.capabilities.filter( (item) => item !== capability @@ -71,7 +73,7 @@ export default function ProfileView() { async function getProfile() { try { - const response = await api.getUserProfile(user); + const response = await api.getUserProfile(userLogin); setProfile(response.data); } catch (error) { redirectToAlert({ @@ -81,7 +83,7 @@ export default function ProfileView() { } } - if (profile.login !== user) return <>; + if (profile.login !== userLogin) return <>; return ( @@ -103,7 +105,7 @@ export default function ProfileView() { changeCapabilities={changeCapabilities} capabilitiesToDelete={capabilitiesToDelete} setCapabilitiesToDelete={setCapabilitiesToDelete} - successMessage={`Capabilities for ${user} successfully changed`} + successMessage={`Capabilities for ${userLogin} successfully changed`} /> ); diff --git a/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.jsx b/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.jsx deleted file mode 100644 index a1de00183..000000000 --- a/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.jsx +++ /dev/null @@ -1,230 +0,0 @@ -import React, { useState, useRef } from "react"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { useLocation, useOutletContext } from "react-router-dom"; - -import { faCopy, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { api } from "@mwdb-web/commons/api"; -import { ConfirmationModal, DateString, ShowIf } from "@mwdb-web/commons/ui"; -import { useViewAlert } from "@mwdb-web/commons/hooks"; - -function KeyNameModal({ isOpen, onConfirm, onClose }) { - const [currentKeyName, setCurrentKeyName] = useState(""); - const ref = useRef(null); - - function handleClose() { - onClose(); - setCurrentKeyName(""); - } - - function handleConfirm() { - onConfirm(currentKeyName); - setCurrentKeyName(""); - } - - return ( - ref.current.focus()} - message={`Name the API key`} - confirmText="Create" - > -
{ - e.preventDefault(); - handleConfirm(); - }} - > - setCurrentKeyName(e.target.value)} - value={currentKeyName} - /> -
-
- ); -} - -export default function ProfileAPIKeys({ profile, getProfile }) { - const location = useLocation(); - const viewAlert = useViewAlert(); - const outletContext = useOutletContext(); - const [currentApiToken, setCurrentApiToken] = useState({}); - const [apiKeyToRemove, setApiKeyToRemove] = useState({}); - const [removeModalOpened, setRemoveModalOpened] = useState(false); - const [apiKeyNameModalOpened, setApiKeyNameModalOpened] = useState(false); - - // Component is reused by Settings - if (profile === undefined) { - profile = outletContext.profile; - getProfile = outletContext.getUser; - } - - function closeKeyNameModal() { - setApiKeyNameModalOpened(false); - } - - async function createApiKey(name) { - try { - const response = await api.apiKeyAdd(profile.login, name); - setCurrentApiToken(response.data); - getProfile(); - viewAlert.setAlert({ - success: "New API key successfully added", - state: { - addedKey: response.data.id, - }, - }); - } catch (error) { - viewAlert.setAlert({ error }); - } - } - - async function removeApiKey(apiKeyId) { - try { - await api.apiKeyRemove(apiKeyId); - setCurrentApiToken({}); - setApiKeyToRemove({}); - getProfile(); - setRemoveModalOpened(false); - viewAlert.setAlert({ - success: "API key successfully removed", - state: { - addedKey: null, - }, - }); - } catch (error) { - viewAlert.setAlert({ error }); - } - } - - if (Object.keys(profile).length === 0) return []; - - return ( -
-

API keys

-

- API keys are just an alternative to password-based - authentication. They are recommended to use for scripts and - other automation instead of plaintext passwords. -

- {!profile.api_keys.length ? ( -

- - There are no API keys. Create the first one using - actions below. - -

- ) : ( - [] - )} - {profile.api_keys.map((apiKey) => ( -
-
-
- - {apiKey.name || apiKey.id} - -
-

- Issued on: {" "} - {apiKey.issuer_login - ? `by ${apiKey.issuer_login}` - : []} -

- { - ev.preventDefault(); - setApiKeyToRemove(apiKey); - setRemoveModalOpened(true); - }} - > - Remove key - - -
-
- - Api key token will be shown only once, - copy its value because it will not be - visible again. - -
-
- {currentApiToken.token} -
- - ev.preventDefault()} - > - Copy - to clipboard - - -
-
-
-
- ))} - Actions: - - setRemoveModalOpened(false)} - onConfirm={(e) => removeApiKey(apiKeyToRemove.id)} - message={`Remove the API key ${apiKeyToRemove.name}?`} - confirmText="Remove" - /> - { - closeKeyNameModal(); - createApiKey(keyName); - }} - /> -
- ); -} diff --git a/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.tsx b/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.tsx new file mode 100644 index 000000000..61eb1cd03 --- /dev/null +++ b/mwdb/web/src/components/Profile/Views/ProfileAPIKeys.tsx @@ -0,0 +1,212 @@ +import { useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { useLocation, useOutletContext } from "react-router-dom"; + +import { faCopy, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { api } from "@mwdb-web/commons/api"; +import { ConfirmationModal, DateString, ShowIf } from "@mwdb-web/commons/ui"; +import { useViewAlert } from "@mwdb-web/commons/hooks"; +import { KeyNameModal } from "../common/KeyNameModal"; +import { ApiKey, Capability, User } from "@mwdb-web/types/types"; + +type OutletContext = { + getUser: () => Promise; + setCapabilitiesToDelete: (cap: Capability) => void; + profile: User; +}; + +type Props = { + profile?: User; + getProfile: () => Promise; +}; + +export default function ProfileAPIKeys({ profile, getProfile }: Props) { + const location = useLocation(); + const viewAlert = useViewAlert(); + const outletContext: OutletContext = useOutletContext(); + const [currentApiToken, setCurrentApiToken] = useState>({}); + const [apiKeyToRemove, setApiKeyToRemove] = useState>({}); + const [removeModalOpened, setRemoveModalOpened] = useState(false); + const [apiKeyNameModalOpened, setApiKeyNameModalOpened] = + useState(false); + + // Component is reused by Settings + if (profile === undefined) { + profile = outletContext.profile; + getProfile = outletContext.getUser; + } + + function closeKeyNameModal() { + setApiKeyNameModalOpened(false); + } + + async function createApiKey(name: string) { + try { + const response = await api.apiKeyAdd(profile!.login, name); + setCurrentApiToken(response.data); + getProfile(); + viewAlert.setAlert({ + success: "New API key successfully added", + state: { + addedKey: response.data.id, + }, + }); + } catch (error) { + viewAlert.setAlert({ error }); + } + } + + async function removeApiKey(apiKeyId: string) { + try { + await api.apiKeyRemove(apiKeyId); + setCurrentApiToken({}); + setApiKeyToRemove({}); + getProfile(); + setRemoveModalOpened(false); + viewAlert.setAlert({ + success: "API key successfully removed", + state: { + addedKey: null, + }, + }); + } catch (error) { + viewAlert.setAlert({ error }); + } + } + + if (Object.keys(profile).length === 0) return <>; + + return ( +
+

API keys

+

+ API keys are just an alternative to password-based + authentication. They are recommended to use for scripts and + other automation instead of plaintext passwords. +

+ {profile.api_keys && !profile.api_keys.length && ( +

+ + There are no API keys. Create the first one using + actions below. + +

+ )} + {profile.api_keys && + profile.api_keys.map((apiKey: ApiKey) => ( +
+
+
+ + {apiKey.name || apiKey.id} + +
+

+ Issued on:{" "} + {" "} + {apiKey.issuer_login + ? `by ${apiKey.issuer_login}` + : []} +

+ { + ev.preventDefault(); + setApiKeyToRemove(apiKey); + setRemoveModalOpened(true); + }} + > + Remove key + + +
+
+ + Api key token will be shown only + once, copy its value because it will + not be visible again. + +
+
+ {currentApiToken.token} +
+ {currentApiToken.token && ( + + + ev.preventDefault() + } + > + {" "} + Copy to clipboard + + + )} +
+
+
+
+ ))} + Actions: + + setRemoveModalOpened(false)} + onConfirm={(e) => + apiKeyToRemove.id && removeApiKey(apiKeyToRemove.id) + } + message={`Remove the API key ${apiKeyToRemove.name}?`} + confirmText="Remove" + /> + { + closeKeyNameModal(); + createApiKey(keyName); + }} + /> +
+ ); +} diff --git a/mwdb/web/src/components/Profile/Views/ProfileCapabilities.jsx b/mwdb/web/src/components/Profile/Views/ProfileCapabilities.jsx deleted file mode 100644 index 8de37a7e2..000000000 --- a/mwdb/web/src/components/Profile/Views/ProfileCapabilities.jsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useState, useEffect, useContext } from "react"; -import { useOutletContext } from "react-router-dom"; -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, AuthContext } from "@mwdb-web/commons/auth"; -import { GroupBadge, ConfirmationModal, Select } from "@mwdb-web/commons/ui"; -import { useViewAlert } from "@mwdb-web/commons/hooks"; -import { useCheckCapabilities } from "@mwdb-web/commons/hooks"; -import { Capability } from "@mwdb-web/types/types"; - -function CapabilitiesTable({ profile }) { - const { user } = useContext(AuthContext); - 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) { - const userOrGroupName = profile.name || profile.login; - const isManageUsersCapability = cap === Capability.manageUsers; - if (isManageUsersCapability && userOrGroupName === user.login) { - return false; - } - return !isNil(profile.login) ? isUserDeleteButtonRender(cap) : true; - } - - if (!profile.capabilities) return []; - return ( - - - {profile.capabilities.sort().map((cap) => ( - - {userHasCapabilities(Capability.manageUsers) && ( - - )} - - - - ))} - -
- {isDeleteButtonRender(cap) && ( - { - ev.preventDefault(); - setCapabilitiesToDelete(cap); - }} - > - - - )} - - {cap} - -
- {capabilitiesList[cap] || "(no description)"} -
-
- {profile.groups && ( - - - Got from: - - {profile.groups - .filter((group) => - group.capabilities.includes(cap) - ) - .map((group, index) => ( - - ))} - - )} -
-
- ); -} - -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) - ); - - 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 onSelectChange(values) { - setChosenCapabilities(values.map((x) => x.value)); - } - - function renderSelectLabel(cap) { - const changed = changedCaps.includes(cap); - return changed ? `* ${cap}` : cap; - } - - 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 ( -
-
- { + return { + value: cap, + label: renderSelectLabel(cap), + }; + })} + value={chosenCapabilities.map((cap) => ({ + value: cap, + label: renderSelectLabel(cap), + }))} + onChange={(newValues) => + onSelectChange(newValues as SelectOptionType[]) + } + closeMenuOnSelect={false} + hideSelectedOptions={false} + /> +
+ + +
+
+ {changedCaps.length > 0 && ( +
+ + * There are pending changes. Click Apply if you want to + commit them or Dismiss otherwise. + +
+ )} + setIsOpen(false)} + onConfirm={changeCapabilities} + /> +
+ ); +} diff --git a/mwdb/web/src/components/Profile/common/CapabilitiesTable.tsx b/mwdb/web/src/components/Profile/common/CapabilitiesTable.tsx new file mode 100644 index 000000000..4278045cc --- /dev/null +++ b/mwdb/web/src/components/Profile/common/CapabilitiesTable.tsx @@ -0,0 +1,101 @@ +import { useContext } from "react"; +import { useOutletContext } from "react-router-dom"; +import { Link } from "react-router-dom"; +import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { find, isNil } from "lodash"; +import { capabilitiesList, AuthContext } from "@mwdb-web/commons/auth"; +import { GroupBadge } from "@mwdb-web/commons/ui"; +import { useCheckCapabilities } from "@mwdb-web/commons/hooks"; +import { Capability, Group, User } from "@mwdb-web/types/types"; +import { ProfileOutletContext } from "@mwdb-web/types/context"; + +type Props = { + profile: User; +}; + +export function CapabilitiesTable({ profile }: Props) { + const { user } = useContext(AuthContext); + const { userHasCapabilities } = useCheckCapabilities(); + const { setCapabilitiesToDelete }: ProfileOutletContext = + useOutletContext(); + + function isUserDeleteButtonRender(cap: Capability) { + const userCap = find(profile.groups, { + name: profile.login, + }); + if (isNil(userCap)) { + return false; + } + return userCap.capabilities.includes(cap); + } + + function isDeleteButtonRender(cap: Capability) { + const userOrGroupName = profile.name || profile.login; + const isManageUsersCapability = cap === Capability.manageUsers; + if (isManageUsersCapability && userOrGroupName === user.login) { + return false; + } + return !isNil(profile.login) ? isUserDeleteButtonRender(cap) : true; + } + + if (!profile.capabilities) return <>; + return ( + + + {profile.capabilities.sort().map((cap) => ( + + {userHasCapabilities(Capability.manageUsers) && ( + + )} + + + + ))} + +
+ {isDeleteButtonRender(cap) && ( + { + ev.preventDefault(); + setCapabilitiesToDelete(cap); + }} + > + + + )} + + {cap} + +
+ {capabilitiesList[cap] || "(no description)"} +
+
+ {profile.groups && ( + + + Got from: + + {profile.groups + .filter((group) => + group.capabilities.includes(cap) + ) + .map( + ( + group: Group, + index: number + ) => ( + + ) + )} + + )} +
+
+ ); +} diff --git a/mwdb/web/src/components/Profile/common/KeyNameModal.tsx b/mwdb/web/src/components/Profile/common/KeyNameModal.tsx new file mode 100644 index 000000000..d3dbf7ef5 --- /dev/null +++ b/mwdb/web/src/components/Profile/common/KeyNameModal.tsx @@ -0,0 +1,51 @@ +import { useState, useRef } from "react"; +import { ConfirmationModal } from "@mwdb-web/commons/ui"; + +type Props = { + isOpen: boolean; + onConfirm: (key: string) => void; + onClose: () => void; +}; + +export function KeyNameModal({ isOpen, onConfirm, onClose }: Props) { + const [currentKeyName, setCurrentKeyName] = useState(""); + const ref = useRef(null); + + function handleClose() { + onClose(); + setCurrentKeyName(""); + } + + function handleConfirm() { + onConfirm(currentKeyName); + setCurrentKeyName(""); + } + + return ( + ref.current?.focus()} + message={`Name the API key`} + confirmText="Create" + > +
{ + e.preventDefault(); + handleConfirm(); + }} + > + setCurrentKeyName(e.target.value)} + value={currentKeyName} + /> +
+
+ ); +} diff --git a/mwdb/web/src/components/Profile/common/ProfileGroupItems.tsx b/mwdb/web/src/components/Profile/common/ProfileGroupItems.tsx new file mode 100644 index 000000000..d3f9b9e2f --- /dev/null +++ b/mwdb/web/src/components/Profile/common/ProfileGroupItems.tsx @@ -0,0 +1,80 @@ +import React, { useState } from "react"; +import { isNil } from "lodash"; + +import { api } from "@mwdb-web/commons/api"; +import { GroupBadge, ConfirmationModal } from "@mwdb-web/commons/ui"; +import { useViewAlert } from "@mwdb-web/commons/hooks"; +import { Group } from "@mwdb-web/types/types"; + +type Props = { + updateWorkspace: () => Promise; + workspace: Group; +}; + +export function ProfileGroupItems({ workspace, updateWorkspace }: Props) { + const { setAlert } = useViewAlert(); + const [isDeleteModalOpen, setDeleteModalOpen] = useState(false); + const [removeUser, setRemoveUser] = useState(null); + + async function handleRemoveMember(login: string | null) { + if (isNil(login)) { + return; + } + try { + await api.removeGroupMember(workspace.name, login); + updateWorkspace(); + setAlert({ + success: `Member '${login}' successfully removed.`, + }); + } catch (error) { + setAlert({ error }); + } finally { + setDeleteModalOpen(false); + setRemoveUser(null); + } + } + + return ( + + {workspace.users + .sort((userA: string, userB: string) => + userA.localeCompare(userB) + ) + .map((login: string, idx: number) => ( + + + + {workspace.admins.includes(login) && " (admin)"} + + + + + + ))} + setDeleteModalOpen(false)} + onConfirm={() => { + handleRemoveMember(removeUser); + }} + message={`Are you sure to delete ${removeUser} user from group?`} + /> + + ); +} diff --git a/mwdb/web/src/components/Profile/common/ProfileItem.tsx b/mwdb/web/src/components/Profile/common/ProfileItem.tsx new file mode 100644 index 000000000..1e33cedbe --- /dev/null +++ b/mwdb/web/src/components/Profile/common/ProfileItem.tsx @@ -0,0 +1,21 @@ +type Props = { + label: string; + value?: number | string | Date; + children?: JSX.Element; +}; + +export function ProfileItem(props: Props) { + if (!props.value) return <>; + + const valueContent = + typeof props.value === "string" || props.value instanceof Date + ? props.value.toString() + : undefined; + + return ( + + {props.label} + {props.children || valueContent} + + ); +} diff --git a/mwdb/web/src/components/ShowObject/Views/ObjectBox.tsx b/mwdb/web/src/components/ShowObject/Views/ObjectBox.tsx index 741cc4806..20d557996 100644 --- a/mwdb/web/src/components/ShowObject/Views/ObjectBox.tsx +++ b/mwdb/web/src/components/ShowObject/Views/ObjectBox.tsx @@ -4,7 +4,7 @@ import { useLocation } from "react-router-dom"; import { TabContext } from "@mwdb-web/commons/ui"; import { useRemotePath } from "@mwdb-web/commons/remotes"; import { useComponentState } from "@mwdb-web/commons/hooks"; -import { TabContextValues } from "@mwdb-web/types/types"; +import { TabContextValues } from "@mwdb-web/types/context"; type Props = { defaultTab: string; diff --git a/mwdb/web/src/types/api.ts b/mwdb/web/src/types/api.ts index 97baddf85..24e2e5a24 100644 --- a/mwdb/web/src/types/api.ts +++ b/mwdb/web/src/types/api.ts @@ -1,5 +1,6 @@ import { AxiosResponse } from "axios"; import { + ApiKey, Attribute, AttributeDefinition, BlobData, @@ -63,15 +64,9 @@ export type AuthRecoverPasswordResponse = Response<{ login: string; }>; -export type AuthGroupsResponse = Response; +export type AuthGroupsResponse = Response<{ groups: Group[] }>; -export type ApiKeyAddResponse = Response<{ - issuer_login: string; - name: string; - id: string; - token: string; - issued_on: string | Date; -}>; +export type ApiKeyAddResponse = Response; export type ApiKeyRemoveResponse = Response; @@ -290,3 +285,21 @@ export type ResubmitKartonAnalysisResponse = Response<{ export type RemoveKartonAnalysisFromObjectResponse = Response; export type EnableSharing3rdPartyResponse = Response; + +export type OauthGetProvidersResponse = Response<{ + providers: string[]; +}>; + +export type OauthGetIdentitiesResponse = Response<{ + providers: string[]; +}>; + +export type OauthGetSingleProviderResponse = Response; + +export type OauthUpdateSingleProviderResponse = Response; + +export type OauthRemoveSingleProviderResponse = Response; + +export type OauthGetLogoutLinkResponse = Response<{ + url: string; +}>; diff --git a/mwdb/web/src/types/context.ts b/mwdb/web/src/types/context.ts new file mode 100644 index 000000000..3b47e76fb --- /dev/null +++ b/mwdb/web/src/types/context.ts @@ -0,0 +1,35 @@ +import { Capability, ServerInfo, User } from "./types"; + +export type TabContextValues = { + tab?: string; + subTab?: string; + getTabLink: (tab: string, subtab?: string) => string; + setComponent: (newComponent: React.ComponentType) => void; + setActions: (actions: JSX.Element[]) => void; +}; + +export type ConfigContextValues = { + config: Partial; + configError: unknown; + isReady: boolean; + update: () => Promise; + pendingUsers: User[]; + getPendingUsers: () => Promise; +}; + +export type AuthContextValues = { + user: User; + isAuthenticated: boolean; + isAdmin: boolean; + hasCapability: (cap: Capability) => boolean; + refreshSession: () => Promise; + updateSession: (newSession: User) => void; + logout: (error?: string) => void; + oAuthLogout: () => Promise; +}; + +export type ProfileOutletContext = { + getUser: () => Promise; + setCapabilitiesToDelete: (cap: Capability) => void; + profile: User; +}; diff --git a/mwdb/web/src/types/props.ts b/mwdb/web/src/types/props.ts index 0a040eb93..7e8222d0b 100644 --- a/mwdb/web/src/types/props.ts +++ b/mwdb/web/src/types/props.ts @@ -11,7 +11,7 @@ export type TagProps = { }; export type GroupBadgeProps = { - group: Group; + group: Partial; basePath?: string; clickable?: boolean; }; @@ -19,3 +19,7 @@ export type GroupBadgeProps = { export type AuthProviderProps = { children: JSX.Element; }; + +export type APIProviderProps = { + children: JSX.Element; +}; diff --git a/mwdb/web/src/types/types.ts b/mwdb/web/src/types/types.ts index d41bb03fe..a12ffba7e 100644 --- a/mwdb/web/src/types/types.ts +++ b/mwdb/web/src/types/types.ts @@ -30,7 +30,8 @@ export enum Capability { export type User = { login: string; - groups: string[] | Group[]; + name?: string; + groups: Group[]; capabilities: Capability[]; additional_info?: string; api_keys?: ApiKey[]; @@ -192,6 +193,7 @@ export type ApiKey = { issued_on: string | Date; issuer_login: string; name: string; + token?: string; }; export type Family = { @@ -219,34 +221,6 @@ export type AxiosServerErrors = AxiosError<{ errors?: Record; }>; -export type TabContextValues = { - tab?: string; - subTab?: string; - getTabLink: (tab: string, subtab?: string) => string; - setComponent: (newComponent: React.ComponentType) => void; - setActions: (actions: JSX.Element[]) => void; -}; - -export type ConfigContextValues = { - config: Partial; - configError: unknown; - isReady: boolean; - update: () => Promise; - pendingUsers: User[]; - getPendingUsers: () => Promise; -}; - -export type AuthContextValues = { - user: User; - isAuthenticated: boolean; - isAdmin: boolean; - hasCapability: (cap: Capability) => boolean; - refreshSession: () => Promise; - updateSession: (newSession: User) => void; - logout: (error?: string) => void; - oAuthLogout: () => Promise; -}; - export type GenericOrJSX = T | JSX.Element; export type ServerInfo = {