diff --git a/packages/console/src/cloud/AppRoutes.tsx b/packages/console/src/cloud/AppRoutes.tsx index 3230e90cc91e..df3b1714e073 100644 --- a/packages/console/src/cloud/AppRoutes.tsx +++ b/packages/console/src/cloud/AppRoutes.tsx @@ -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'; @@ -19,12 +23,17 @@ function AppRoutes() { } /> } /> }> - {isCloud && ( - } - /> - )} + } + /> + + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/packages/console/src/components/Topbar/UserInfo/index.module.scss b/packages/console/src/components/Topbar/UserInfo/index.module.scss index 489df1751f58..2d2582a01493 100644 --- a/packages/console/src/components/Topbar/UserInfo/index.module.scss +++ b/packages/console/src/components/Topbar/UserInfo/index.module.scss @@ -48,6 +48,11 @@ } .icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; color: var(--color-text-secondary); } diff --git a/packages/console/src/components/Topbar/UserInfo/index.tsx b/packages/console/src/components/Topbar/UserInfo/index.tsx index 7dd2a1c25a64..9f3c54ff03cc 100644 --- a/packages/console/src/components/Topbar/UserInfo/index.tsx +++ b/packages/console/src/components/Topbar/UserInfo/index.tsx @@ -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'; @@ -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(null); @@ -77,10 +79,19 @@ function UserInfo() { className={classNames(styles.dropdownItem, isLoading && styles.loading)} 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')} + +
+ +
- {isCloud && } - {!isCloud && ( + {isCloud && hasTenantSelector !== false && } + {!isCloud && hasTitle !== false && ( <>
{t('title')}
diff --git a/packages/console/src/containers/ConsoleRoutes/index.tsx b/packages/console/src/containers/ConsoleRoutes/index.tsx index ec49548bc1a8..6d32e53ac424 100644 --- a/packages/console/src/containers/ConsoleRoutes/index.tsx +++ b/packages/console/src/containers/ConsoleRoutes/index.tsx @@ -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'; @@ -47,6 +52,13 @@ export function ConsoleRoutes() { } /> }> } /> + + } /> + } /> + } /> + } /> + } /> + }> {isCloud && ( = Object.freeze([ diff --git a/packages/console/src/hooks/use-user-assets-service.ts b/packages/console/src/hooks/use-user-assets-service.ts index e781ff149d8b..039a55da9f93 100644 --- a/packages/console/src/hooks/use-user-assets-service.ts +++ b/packages/console/src/hooks/use-user-assets-service.ts @@ -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(isCloud && isProfilePage ? adminApi : api); const { data, error } = useSWRImmutable( - 'api/user-assets/service-status' + 'api/user-assets/service-status', + fetcher ); return { diff --git a/packages/console/src/pages/Profile/index.module.scss b/packages/console/src/pages/Profile/index.module.scss index 5a963e324d74..26226a6afcd4 100644 --- a/packages/console/src/pages/Profile/index.module.scss +++ b/packages/console/src/pages/Profile/index.module.scss @@ -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); + } } } diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index ad84e5980d60..bd51796ff895 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -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'; @@ -43,58 +45,67 @@ function Profile() { const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading; return ( -
- -
- -
- {showLoadingSkeleton && } - {user && !showLoadingSkeleton && ( -
- - {isCloud && } - - (value ? ******** : ), - action: { - name: 'profile.change', - handler: () => { - navigate(user.hasPassword ? 'verify-password' : 'change-password', { - state: { email: user.primaryEmail, action: 'changePassword' }, - }); +
+ + +
+ +
+ +
+ {showLoadingSkeleton && } + {user && !showLoadingSkeleton && ( +
+ + {isCloud && ( + + )} + + (value ? ******** : ), + action: { + name: 'profile.change', + handler: () => { + navigate(user.hasPassword ? 'verify-password' : 'change-password', { + state: { email: user.primaryEmail, action: 'changePassword' }, + }); + }, + }, }, - }, - }, - ]} - /> - - {isCloud && ( - -
-
{t('profile.delete_account.description')}
-
- { - setShowDeleteAccountModal(false); - }} - /> -
+ + {isCloud && ( + +
+
+ {t('profile.delete_account.description')} +
+
+ { + setShowDeleteAccountModal(false); + }} + /> +
+ )} +
)}
- )} +
); }