diff --git a/src/ui/components/userDetail/context/UserDetailContext.tsx b/src/ui/components/userDetail/context/UserDetailContext.tsx new file mode 100644 index 00000000..d39fba68 --- /dev/null +++ b/src/ui/components/userDetail/context/UserDetailContext.tsx @@ -0,0 +1,44 @@ +/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import { PropsWithChildren, createContext, useContext } from "react"; + +type UserDetailContextType = { + showLoadingOverlay: () => void; + hideLoadingOverlay: () => void; +}; + +type IncomingProps = { + showLoadingOverlay: () => void; + hideLoadingOverlay: () => void; +}; + +type Props = PropsWithChildren; + +const UserDetailContext = createContext(undefined); + +export const useUserDetailContext = () => { + const context = useContext(UserDetailContext); + if (!context) throw "Context must be used within a provider!"; + return context; +}; + +export const UserDetailContextProvider: React.FC = (props: Props) => { + return ( + + {props.children} + + ); +}; diff --git a/src/ui/components/userDetail/userDetail.scss b/src/ui/components/userDetail/userDetail.scss index bdea311a..c41bf9fe 100644 --- a/src/ui/components/userDetail/userDetail.scss +++ b/src/ui/components/userDetail/userDetail.scss @@ -16,6 +16,69 @@ $container-padding-horizontal: 40; $container-width: 829; +.user-detail-page-loader { + min-height: 80vh; + display: flex; + justify-content: center; + align-items: center; +} + +.full-screen-loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.loader-container { + background-color: var(--color-window-bg); + padding: 2px; + display: flex; + border-radius: 50%; +} + +.loader { + border: 16px solid #f3f3f3; /* Light grey */ + border-top: 16px solid #ff9933; /* Blue */ + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} + +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + } +} + .user-detail { --badge-bg-color: rgb(197, 224, 253); --copy-text-color: rgb(214, 80, 120); diff --git a/src/ui/components/userDetail/userDetail.tsx b/src/ui/components/userDetail/userDetail.tsx index e3b065e2..d500dc55 100644 --- a/src/ui/components/userDetail/userDetail.tsx +++ b/src/ui/components/userDetail/userDetail.tsx @@ -25,6 +25,7 @@ import { PopupContentContext } from "../../contexts/PopupContentContext"; import { useTenantsListContext } from "../../contexts/TenantsListContext"; import { EmailVerificationStatus, UserRecipeType, UserWithRecipeId } from "../../pages/usersList/types"; import { OnSelectUserFunction } from "../usersListTable/UsersListTable"; +import { UserDetailContextProvider } from "./context/UserDetailContext"; import { UserTenantsList } from "./tenantList/UserTenantsList"; import "./userDetail.scss"; import { getUpdateUserToast } from "./userDetailForm"; @@ -55,6 +56,7 @@ export const UserDetail: React.FC = (props) => { const [emailVerificationStatus, setEmailVerificationStatus] = useState( undefined ); + const [shouldShowLoadingOverlay, setShowLoadingOverlay] = useState(false); const { getUser, updateUserInformation } = useUserService(); @@ -152,14 +154,28 @@ export const UserDetail: React.FC = (props) => { }, [fetchEmailVerificationStatus]); const refetchAllData = async () => { + setShowLoadingOverlay(true); await loadUserDetail(); await fetchUserMetaData(); await fetchSession(); await fetchEmailVerificationStatus(); + setShowLoadingOverlay(false); + }; + + const showLoadingOverlay = () => { + setShowLoadingOverlay(true); + }; + + const hideLoadingOverlay = () => { + setShowLoadingOverlay(false); }; if (userDetail === undefined) { - return <>; + return ( +
+
+
+ ); } if (userDetail.status === "NO_USER_FOUND_ERROR") { @@ -191,43 +207,54 @@ export const UserDetail: React.FC = (props) => { } return ( -
-
- + +
+ {shouldShowLoadingOverlay && ( +
+
+
+
+
+ )} +
+ +
+ + + + + + + +
- - - - - - - - -
+ ); }; diff --git a/src/ui/components/userDetail/userDetailInfoGrid.tsx b/src/ui/components/userDetail/userDetailInfoGrid.tsx index 5fbc3961..e22fa759 100644 --- a/src/ui/components/userDetail/userDetailInfoGrid.tsx +++ b/src/ui/components/userDetail/userDetailInfoGrid.tsx @@ -16,6 +16,7 @@ import { PhoneNumberInput } from "../phoneNumber/PhoneNumberInput"; import TooltipContainer from "../tooltip/tooltip"; import { UserRecipePill } from "../usersListTable/UsersListTable"; import { UserDetailNameField } from "./components/nameField/nameField"; +import { useUserDetailContext } from "./context/UserDetailContext"; import { UserDetailProps } from "./userDetail"; import { getUserChangePasswordPopupProps } from "./userDetailForm"; @@ -221,8 +222,10 @@ export const UserDetailInfoGrid: FC = (props) => { const { recipeId } = userState; const { firstName, lastName, timeJoined, email } = userState.user; const [isEditing, setIsEditing] = useState(false); + const { showLoadingOverlay, hideLoadingOverlay } = useUserDetailContext(); const onSave = useCallback(async () => { + showLoadingOverlay(); const response = await onUpdateCallback(userDetail.user.id, userState); if (response.status === "OK") { @@ -245,6 +248,8 @@ export const UserDetailInfoGrid: FC = (props) => { setPhoneErrorFromAPI(response.error); } } + + hideLoadingOverlay(); }, [onUpdateCallback, userState, userDetail]); const updateUserDataState = useCallback((updatedUser: Partial) => { diff --git a/src/ui/components/userDetail/userDetailSessionList.tsx b/src/ui/components/userDetail/userDetailSessionList.tsx index 1497d09f..61894ed0 100644 --- a/src/ui/components/userDetail/userDetailSessionList.tsx +++ b/src/ui/components/userDetail/userDetailSessionList.tsx @@ -15,6 +15,7 @@ import useSessionsForUserService from "../../../api/user/sessions"; import { formatLongDate, getFormattedLongDateWithoutTime } from "../../../utils"; import { PlaceholderTableRows } from "../usersListTable/UsersListTable"; +import { useUserDetailContext } from "./context/UserDetailContext"; import "./userDetailSessionList.scss"; export type UserDetailsSessionListProps = { @@ -28,14 +29,20 @@ export const UserDetailsSessionList: React.FC = ({ }: UserDetailsSessionListProps) => { const sessionCountText = sessionList === undefined ? "" : `(TOTAL NO OF SESSIONS: ${sessionList.length})`; const { deleteSessionsForUser } = useSessionsForUserService(); + const { hideLoadingOverlay, showLoadingOverlay } = useUserDetailContext(); const revokeAllSessions = async () => { - if (sessionList === undefined) { - return; - } + showLoadingOverlay(); + try { + if (sessionList === undefined) { + return; + } - const allSessionHandles: string[] = sessionList.map((item) => item.sessionHandle); - await deleteSessionsForUser(allSessionHandles); - await refetchData(); + const allSessionHandles: string[] = sessionList.map((item) => item.sessionHandle); + await deleteSessionsForUser(allSessionHandles); + await refetchData(); + } finally { + hideLoadingOverlay(); + } }; return ( diff --git a/src/ui/components/userDetail/userMetaDataSection.tsx b/src/ui/components/userDetail/userMetaDataSection.tsx index afb25fdf..6c16dca3 100644 --- a/src/ui/components/userDetail/userMetaDataSection.tsx +++ b/src/ui/components/userDetail/userMetaDataSection.tsx @@ -19,6 +19,7 @@ import { useEffect, useState } from "react"; import useMetadataService from "../../../api/user/metadata"; import { getImageUrl } from "../../../utils"; import IconButton from "../common/iconButton"; +import { useUserDetailContext } from "./context/UserDetailContext"; import "./userMetaDataSection.scss"; export type UserMetaDataSectionProps = { @@ -35,6 +36,7 @@ export const UserMetaDataSection: React.FC = ({ const [isEditing, setIsEditing] = useState(false); const [metadataForEditing, setMetaDataForEditing] = useState(metadata); const [metaDataUpdateError, setMetaDataUpdateError] = useState(undefined); + const { hideLoadingOverlay, showLoadingOverlay } = useUserDetailContext(); const { updateUserMetaData } = useMetadataService(); @@ -108,6 +110,7 @@ export const UserMetaDataSection: React.FC = ({ }; const onSave = async () => { + showLoadingOverlay(); try { try { // We json parse here to make sure its a valid JSON so that the API does not need to throw a 400 @@ -132,6 +135,8 @@ export const UserMetaDataSection: React.FC = ({ } setMetaDataUpdateError(errorMessage); + } finally { + hideLoadingOverlay(); } };