Skip to content

Commit

Permalink
fix(console): make profile a tenant-independent page
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao committed Apr 16, 2024
1 parent de47d6a commit d9c86ea
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 77 deletions.
23 changes: 16 additions & 7 deletions packages/console/src/cloud/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Route, Routes } from 'react-router-dom';

import { isCloud } from '@/consts/env';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider';
import AcceptInvitation from '@/pages/AcceptInvitation';
import Callback from '@/pages/Callback';
import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
import Profile from '@/pages/Profile';
import ChangePasswordModal from '@/pages/Profile/containers/ChangePasswordModal';
import LinkEmailModal from '@/pages/Profile/containers/LinkEmailModal';
import VerificationCodeModal from '@/pages/Profile/containers/VerificationCodeModal';
import VerifyPasswordModal from '@/pages/Profile/containers/VerifyPasswordModal';

import * as styles from './AppRoutes.module.scss';
import Main from './pages/Main';
Expand All @@ -19,12 +23,17 @@ function AppRoutes() {
<Route path={GlobalAnonymousRoute.Callback} element={<Callback />} />
<Route path={GlobalAnonymousRoute.SocialDemoCallback} element={<SocialDemoCallback />} />
<Route element={<ProtectedRoutes />}>
{isCloud && (
<Route
path={`${GlobalRoute.AcceptInvitation}/:invitationId`}
element={<AcceptInvitation />}
/>
)}
<Route
path={`${GlobalRoute.AcceptInvitation}/:invitationId`}
element={<AcceptInvitation />}
/>
<Route path={GlobalRoute.Profile}>
<Route index element={<Profile />} />
<Route path="verify-password" element={<VerifyPasswordModal />} />
<Route path="change-password" element={<ChangePasswordModal />} />
<Route path="link-email" element={<LinkEmailModal />} />
<Route path="verification-code" element={<VerificationCodeModal />} />
</Route>
<Route path={GlobalRoute.CheckoutSuccessCallback} element={<CheckoutSuccessCallback />} />
<Route index element={<Main />} />
</Route>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
}

.icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: var(--color-text-secondary);
}

Expand Down
15 changes: 13 additions & 2 deletions packages/console/src/components/Topbar/UserInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import classNames from 'classnames';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import ExternalLinkIcon from '@/assets/icons/external-link.svg';
import Globe from '@/assets/icons/globe.svg';
import Palette from '@/assets/icons/palette.svg';
import Profile from '@/assets/icons/profile.svg';
import SignOut from '@/assets/icons/sign-out.svg';
import UserAvatar from '@/components/UserAvatar';
import UserInfoCard from '@/components/UserInfoCard';
import { isCloud } from '@/consts/env';
import Divider from '@/ds-components/Divider';
import Dropdown, { DropdownItem } from '@/ds-components/Dropdown';
import Spacer from '@/ds-components/Spacer';
Expand All @@ -28,7 +30,7 @@ import * as styles from './index.module.scss';

function UserInfo() {
const { signOut } = useLogto();
const { navigate } = useTenantPathname();
const { getUrl } = useTenantPathname();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { user, isLoading: isLoadingUser } = useCurrentUser();
const anchorRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -77,10 +79,19 @@ function UserInfo() {
className={classNames(styles.dropdownItem, isLoading && styles.loading)}
icon={<Profile className={styles.icon} />}
onClick={() => {
navigate('/profile');
// In OSS version, there will be a `/console` context path in the URL.
const urlWithContextPath = getUrl('/profile');

// Open the profile page in a new tab. In Logto Cloud, the profile page is not nested in the tenant independent,
// whereas in OSS version, it is under the `/console` context path.
window.open(isCloud ? '/profile' : urlWithContextPath, '_blank');
}}
>
{t('menu.profile')}
<Spacer />
<div className={styles.icon}>
<ExternalLinkIcon />
</div>
</DropdownItem>
<Divider />
<SubMenu
Expand Down
8 changes: 5 additions & 3 deletions packages/console/src/components/Topbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ import * as styles from './index.module.scss';

type Props = {
className?: string;
hasTenantSelector?: false;
hasTitle?: false;
};

function Topbar({ className }: Props) {
function Topbar({ className, hasTenantSelector, hasTitle }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const LogtoLogo = isCloud ? CloudLogo : Logo;

return (
<div className={classNames(styles.topbar, className)}>
<LogtoLogo className={styles.logo} />
{isCloud && <TenantSelector />}
{!isCloud && (
{isCloud && hasTenantSelector !== false && <TenantSelector />}
{!isCloud && hasTitle !== false && (
<>
<div className={styles.line} />
<div className={styles.text}>{t('title')}</div>
Expand Down
12 changes: 12 additions & 0 deletions packages/console/src/containers/ConsoleRoutes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import Toast from '@/ds-components/Toast';
import useSwrOptions from '@/hooks/use-swr-options';
import Callback from '@/pages/Callback';
import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
import Profile from '@/pages/Profile';
import ChangePasswordModal from '@/pages/Profile/containers/ChangePasswordModal';
import HandleSocialCallback from '@/pages/Profile/containers/HandleSocialCallback';
import LinkEmailModal from '@/pages/Profile/containers/LinkEmailModal';
import VerificationCodeModal from '@/pages/Profile/containers/VerificationCodeModal';
import VerifyPasswordModal from '@/pages/Profile/containers/VerifyPasswordModal';
import Welcome from '@/pages/Welcome';
import { dropLeadingSlash } from '@/utils/url';

Expand Down Expand Up @@ -47,6 +52,13 @@ export function ConsoleRoutes() {
<Route path="welcome" element={<Welcome />} />
<Route element={<ProtectedRoutes />}>
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route path={dropLeadingSlash(GlobalRoute.Profile)}>
<Route index element={<Profile />} />
<Route path="verify-password" element={<VerifyPasswordModal />} />
<Route path="change-password" element={<ChangePasswordModal />} />
<Route path="link-email" element={<LinkEmailModal />} />
<Route path="verification-code" element={<VerificationCodeModal />} />
</Route>
<Route element={<TenantAccess />}>
{isCloud && (
<Route
Expand Down
1 change: 1 addition & 0 deletions packages/console/src/contexts/TenantsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum GlobalAnonymousRoute {
export enum GlobalRoute {
CheckoutSuccessCallback = '/checkout-success-callback',
AcceptInvitation = '/accept',
Profile = '/profile',
}

const reservedRoutes: Readonly<string[]> = Object.freeze([
Expand Down
20 changes: 18 additions & 2 deletions packages/console/src/hooks/use-user-assets-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import type { UserAssetsServiceStatus } from '@logto/schemas';
import { useLocation } from 'react-router-dom';
import useSWRImmutable from 'swr/immutable';

import type { RequestError } from './use-api';
import { adminTenantEndpoint, meApi } from '@/consts';
import { isCloud } from '@/consts/env';
import { GlobalRoute } from '@/contexts/TenantsProvider';

import useApi, { useStaticApi, type RequestError } from './use-api';
import useSwrFetcher from './use-swr-fetcher';

const useUserAssetsService = () => {
const adminApi = useStaticApi({
prefixUrl: adminTenantEndpoint,
resourceIndicator: meApi.indicator,
});
const api = useApi();
const { pathname } = useLocation();
const isProfilePage =
pathname === GlobalRoute.Profile || pathname.startsWith(GlobalRoute.Profile + '/');
const fetcher = useSwrFetcher<UserAssetsServiceStatus>(isCloud && isProfilePage ? adminApi : api);
const { data, error } = useSWRImmutable<UserAssetsServiceStatus, RequestError>(
'api/user-assets/service-status'
'api/user-assets/service-status',
fetcher
);

return {
Expand Down
49 changes: 34 additions & 15 deletions packages/console/src/pages/Profile/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
@use '@/scss/underscore' as _;

.content {
margin-top: _.unit(4);
padding-bottom: _.unit(6);
.pageContainer {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
height: 100%;

.scrollable {
width: 100%;
}

.wrapper {
@include _.main-content-width;
width: 100%;
padding: _.unit(3) _.unit(6) 0;
}

> div + div {
.content {
width: 100%;
margin-top: _.unit(4);
padding-bottom: _.unit(6);

> div + div {
margin-top: _.unit(4);
}
}
}

.deleteAccount {
flex: 1;
display: flex;
align-items: center;
border: 1px solid var(--color-divider);
border-radius: 8px;
padding: _.unit(4);
.deleteAccount {
flex: 1;
display: flex;
align-items: center;
border: 1px solid var(--color-divider);
border-radius: 8px;
padding: _.unit(4);

.description {
font: var(--font-body-2);
margin-right: _.unit(2);
.description {
font: var(--font-body-2);
margin-right: _.unit(2);
}
}
}
107 changes: 59 additions & 48 deletions packages/console/src/pages/Profile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import useSWRImmutable from 'swr/immutable';

import FormCard from '@/components/FormCard';
import PageMeta from '@/components/PageMeta';
import Topbar from '@/components/Topbar';
import { adminTenantEndpoint, meApi } from '@/consts';
import { isCloud } from '@/consts/env';
import Button from '@/ds-components/Button';
import CardTitle from '@/ds-components/CardTitle';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import type { RequestError } from '@/hooks/use-api';
import { useStaticApi } from '@/hooks/use-api';
import useCurrentUser from '@/hooks/use-current-user';
Expand Down Expand Up @@ -43,58 +45,67 @@ function Profile() {
const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading;

return (
<div className={pageLayout.container}>
<PageMeta titleKey="profile.page_title" />
<div className={pageLayout.headline}>
<CardTitle title="profile.title" subtitle="profile.description" />
</div>
{showLoadingSkeleton && <Skeleton />}
{user && !showLoadingSkeleton && (
<div className={styles.content}>
<BasicUserInfoSection user={user} onUpdate={reload} />
{isCloud && <LinkAccountSection user={user} connectors={connectors} onUpdate={reload} />}
<FormCard title="profile.password.title">
<CardContent
title="profile.password.password_setting"
data={[
{
key: 'password',
label: 'profile.password.password',
value: user.hasPassword,
renderer: (value) => (value ? <span>********</span> : <NotSet />),
action: {
name: 'profile.change',
handler: () => {
navigate(user.hasPassword ? 'verify-password' : 'change-password', {
state: { email: user.primaryEmail, action: 'changePassword' },
});
<div className={styles.pageContainer}>
<Topbar hasTenantSelector={false} hasTitle={false} />
<OverlayScrollbar className={styles.scrollable}>
<div className={styles.wrapper}>
<PageMeta titleKey="profile.page_title" />
<div className={pageLayout.headline}>
<CardTitle title="profile.title" subtitle="profile.description" />
</div>
{showLoadingSkeleton && <Skeleton />}
{user && !showLoadingSkeleton && (
<div className={styles.content}>
<BasicUserInfoSection user={user} onUpdate={reload} />
{isCloud && (
<LinkAccountSection user={user} connectors={connectors} onUpdate={reload} />
)}
<FormCard title="profile.password.title">
<CardContent
title="profile.password.password_setting"
data={[
{
key: 'password',
label: 'profile.password.password',
value: user.hasPassword,
renderer: (value) => (value ? <span>********</span> : <NotSet />),
action: {
name: 'profile.change',
handler: () => {
navigate(user.hasPassword ? 'verify-password' : 'change-password', {
state: { email: user.primaryEmail, action: 'changePassword' },
});
},
},
},
},
},
]}
/>
</FormCard>
{isCloud && (
<FormCard title="profile.delete_account.title">
<div className={styles.deleteAccount}>
<div className={styles.description}>{t('profile.delete_account.description')}</div>
<Button
title="profile.delete_account.button"
onClick={() => {
setShowDeleteAccountModal(true);
}}
]}
/>
</div>
<DeleteAccountModal
isOpen={showDeleteAccountModal}
onClose={() => {
setShowDeleteAccountModal(false);
}}
/>
</FormCard>
</FormCard>
{isCloud && (
<FormCard title="profile.delete_account.title">
<div className={styles.deleteAccount}>
<div className={styles.description}>
{t('profile.delete_account.description')}
</div>
<Button
title="profile.delete_account.button"
onClick={() => {
setShowDeleteAccountModal(true);
}}
/>
</div>
<DeleteAccountModal
isOpen={showDeleteAccountModal}
onClose={() => {
setShowDeleteAccountModal(false);
}}
/>
</FormCard>
)}
</div>
)}
</div>
)}
</OverlayScrollbar>
</div>
);
}
Expand Down

0 comments on commit d9c86ea

Please sign in to comment.