Skip to content

Commit

Permalink
Add loading states for user details
Browse files Browse the repository at this point in the history
  • Loading branch information
nkshah2 committed Jul 18, 2023
1 parent e054ac5 commit 821b598
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 44 deletions.
2 changes: 1 addition & 1 deletion build/static/css/main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/css/main.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js.map

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions src/ui/components/userDetail/context/UserDetailContext.tsx
Original file line number Diff line number Diff line change
@@ -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<IncomingProps>;

const UserDetailContext = createContext<UserDetailContextType | undefined>(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: Props) => {
return (
<UserDetailContext.Provider
value={{ showLoadingOverlay: props.showLoadingOverlay, hideLoadingOverlay: props.hideLoadingOverlay }}>
{props.children}
</UserDetailContext.Provider>
);
};
63 changes: 63 additions & 0 deletions src/ui/components/userDetail/userDetail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
93 changes: 59 additions & 34 deletions src/ui/components/userDetail/userDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getImageUrl, getRecipeNameFromid } from "../../../utils";
import { PopupContentContext } from "../../contexts/PopupContentContext";
import { EmailVerificationStatus, UserRecipeType, UserWithRecipeId } from "../../pages/usersList/types";
import { OnSelectUserFunction } from "../usersListTable/UsersListTable";
import { UserDetailContextProvider } from "./context/UserDetailContext";
import "./userDetail.scss";
import { getUpdateUserToast } from "./userDetailForm";
import UserDetailHeader from "./userDetailHeader";
Expand All @@ -47,6 +48,7 @@ export const UserDetail: React.FC<UserDetailProps> = (props) => {
const [emailVerificationStatus, setEmailVerificationStatus] = useState<EmailVerificationStatus | undefined>(
undefined
);
const [shouldShowLoadingOverlay, setShowLoadingOverlay] = useState<boolean>(false);

const { getUser, updateUserInformation } = useUserService();

Expand Down Expand Up @@ -128,8 +130,20 @@ export const UserDetail: React.FC<UserDetailProps> = (props) => {
await fetchEmailVerificationStatus();
};

const showLoadingOverlay = () => {
setShowLoadingOverlay(true);
};

const hideLoadingOverlay = () => {
setShowLoadingOverlay(false);
};

if (userDetail === undefined) {
return <></>;
return (
<div className="user-detail-page-loader">
<div className="loader"></div>
</div>
);
}

if (userDetail.status === "NO_USER_FOUND_ERROR") {
Expand Down Expand Up @@ -161,40 +175,51 @@ export const UserDetail: React.FC<UserDetailProps> = (props) => {
}

return (
<div className="user-detail">
<div className="user-detail__navigation">
<button
className="button flat"
onClick={onBackButtonClicked}>
<img
src={getImageUrl("left-arrow-dark.svg")}
alt="Back to all users"
/>
<span>Back to all users</span>
</button>
<UserDetailContextProvider
showLoadingOverlay={showLoadingOverlay}
hideLoadingOverlay={hideLoadingOverlay}>
<div className="user-detail">
{shouldShowLoadingOverlay && (
<div className="full-screen-loading-overlay">
<div className="loader-container">
<div className="loader"></div>
</div>
</div>
)}
<div className="user-detail__navigation">
<button
className="button flat"
onClick={onBackButtonClicked}>
<img
src={getImageUrl("left-arrow-dark.svg")}
alt="Back to all users"
/>
<span>Back to all users</span>
</button>
</div>
<UserDetailHeader
userDetail={userDetail.user}
{...props}
/>
<UserDetailInfoGrid
userDetail={userDetail.user}
refetchData={refetchAllData}
onUpdateCallback={updateUser}
emailVerificationStatus={emailVerificationStatus}
{...props}
/>
<UserMetaDataSection
metadata={userMetaData}
userId={user}
refetchData={refetchAllData}
/>

<UserDetailsSessionList
sessionList={sessionList}
refetchData={refetchAllData}
/>
</div>
<UserDetailHeader
userDetail={userDetail.user}
{...props}
/>
<UserDetailInfoGrid
userDetail={userDetail.user}
refetchData={refetchAllData}
onUpdateCallback={updateUser}
emailVerificationStatus={emailVerificationStatus}
{...props}
/>
<UserMetaDataSection
metadata={userMetaData}
userId={user}
refetchData={refetchAllData}
/>

<UserDetailsSessionList
sessionList={sessionList}
refetchData={refetchAllData}
/>
</div>
</UserDetailContextProvider>
);
};

Expand Down
4 changes: 4 additions & 0 deletions src/ui/components/userDetail/userDetailInfoGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -221,8 +222,10 @@ export const UserDetailInfoGrid: FC<UserDetailInfoGridProps> = (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") {
Expand All @@ -245,6 +248,7 @@ export const UserDetailInfoGrid: FC<UserDetailInfoGridProps> = (props) => {
setPhoneErrorFromAPI(response.error);
}
}
hideLoadingOverlay();
}, [onUpdateCallback, userState, userDetail]);

const updateUserDataState = useCallback((updatedUser: Partial<UserWithRecipeId["user"]>) => {
Expand Down
19 changes: 13 additions & 6 deletions src/ui/components/userDetail/userDetailSessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -28,14 +29,20 @@ export const UserDetailsSessionList: React.FC<UserDetailsSessionListProps> = ({
}: UserDetailsSessionListProps) => {
const sessionCountText = sessionList === undefined ? "" : `(TOTAL NO OF SESSIONS: ${sessionList.length})`;
const { deleteSessionsForUser } = useSessionsForUserService();
const { showLoadingOverlay, hideLoadingOverlay } = 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 (
Expand Down
5 changes: 5 additions & 0 deletions src/ui/components/userDetail/userMetaDataSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -35,6 +36,7 @@ export const UserMetaDataSection: React.FC<UserMetaDataSectionProps> = ({
const [isEditing, setIsEditing] = useState<boolean>(false);
const [metadataForEditing, setMetaDataForEditing] = useState(metadata);
const [metaDataUpdateError, setMetaDataUpdateError] = useState<string | undefined>(undefined);
const { showLoadingOverlay, hideLoadingOverlay } = useUserDetailContext();

const { updateUserMetaData } = useMetadataService();

Expand Down Expand Up @@ -108,6 +110,7 @@ export const UserMetaDataSection: React.FC<UserMetaDataSectionProps> = ({
};

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
Expand All @@ -132,6 +135,8 @@ export const UserMetaDataSection: React.FC<UserMetaDataSectionProps> = ({
}

setMetaDataUpdateError(errorMessage);
} finally {
hideLoadingOverlay();
}
};

Expand Down

0 comments on commit 821b598

Please sign in to comment.