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 May 7, 2024
1 parent f57e21f commit 6615f63
Show file tree
Hide file tree
Showing 16 changed files with 287 additions and 96 deletions.
17 changes: 9 additions & 8 deletions packages/console/src/cloud/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useRoutes } from 'react-router-dom';

import { isCloud } from '@/consts/env';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider';
import { profile } from '@/hooks/use-console-routes/routes/profile';
import AcceptInvitation from '@/pages/AcceptInvitation';
import Callback from '@/pages/Callback';
import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
Expand All @@ -13,18 +13,19 @@ import SocialDemoCallback from './pages/SocialDemoCallback';

/** Renders necessary routes when the user is not in a tenant context. */
function AppRoutes() {
const profileRoutes = useRoutes(profile);

return (
<div className={styles.app}>
<Routes>
<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}>{profileRoutes}</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 profileRouteWithConsoleContext = 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' : profileRouteWithConsoleContext, '_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 = {
readonly className?: string;
readonly hasTenantSelector?: false;
readonly 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
6 changes: 5 additions & 1 deletion packages/console/src/containers/ConsoleRoutes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ossConsolePath } from '@logto/schemas';
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
import { Navigate, Outlet, Route, Routes, useRoutes } from 'react-router-dom';
import { SWRConfig } from 'swr';

import { isCloud } from '@/consts/env';
Expand All @@ -10,6 +10,7 @@ import ProtectedRoutes from '@/containers/ProtectedRoutes';
import TenantAccess from '@/containers/TenantAccess';
import { GlobalRoute } from '@/contexts/TenantsProvider';
import Toast from '@/ds-components/Toast';
import { profile } from '@/hooks/use-console-routes/routes/profile';
import useSwrOptions from '@/hooks/use-swr-options';
import Callback from '@/pages/Callback';
import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
Expand All @@ -31,6 +32,8 @@ function Layout() {
}

export function ConsoleRoutes() {
const profileRoutes = useRoutes(profile);

return (
<Routes>
{/**
Expand All @@ -44,6 +47,7 @@ export function ConsoleRoutes() {
<Route path="welcome" element={<Welcome />} />
<Route element={<ProtectedRoutes />}>
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route path={dropLeadingSlash(GlobalRoute.Profile)}>{profileRoutes}</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
17 changes: 15 additions & 2 deletions packages/console/src/ds-components/Uploader/FileUploader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AllowedUploadMimeType, UserAssets } from '@logto/schemas';
import { maxUploadFileSize } from '@logto/schemas';
import classNames from 'classnames';
import { type KyInstance } from 'ky';
import { useCallback, useEffect, useState } from 'react';
import { type FileRejection, useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
Expand All @@ -20,6 +21,15 @@ export type Props = {
readonly onCompleted: (fileUrl: string) => void;
readonly onUploadErrorChange: (errorMessage?: string) => void;
readonly className?: string;
/**
* Specify which API instance to use for the upload request. For example, you can use admin tenant API instead.
* Defaults to the return value of `useApi()`.
*/
readonly apiInstance?: KyInstance;
/**
* Specify the URL to upload the file to. Defaults to `api/user-assets`.
*/
readonly uploadUrl?: string;
};

function FileUploader({
Expand All @@ -29,6 +39,8 @@ function FileUploader({
onCompleted,
onUploadErrorChange,
className,
apiInstance,
uploadUrl = 'api/user-assets',
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isUploading, setIsUploading] = useState(false);
Expand Down Expand Up @@ -91,7 +103,8 @@ function FileUploader({

try {
setIsUploading(true);
const { url } = await api.post('api/user-assets', { body: formData }).json<UserAssets>();
const uploadApi = apiInstance ?? api;
const { url } = await uploadApi.post(uploadUrl, { body: formData }).json<UserAssets>();

onCompleted(url);
} catch {
Expand All @@ -100,7 +113,7 @@ function FileUploader({
setIsUploading(false);
}
},
[allowedMimeTypes, api, maxSize, onCompleted, t]
[allowedMimeTypes, apiInstance, api, maxSize, onCompleted, t]
);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { Props as ImageUploaderProps } from '../ImageUploader';

import * as styles from './index.module.scss';

type Props = Pick<ImageUploaderProps, 'name' | 'value' | 'actionDescription'> & {
type Props = Omit<ImageUploaderProps, 'onDelete' | 'onCompleted' | 'onUploadErrorChange'> & {
readonly onChange: (value: string) => void;
readonly allowedMimeTypes?: UserAssetsServiceStatus['allowUploadMimeTypes'];
};
Expand Down
2 changes: 0 additions & 2 deletions packages/console/src/hooks/use-console-routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { customizeJwt } from './routes/customize-jwt';
import { enterpriseSso } from './routes/enterprise-sso';
import { organizationTemplate } from './routes/organization-template';
import { organizations } from './routes/organizations';
import { profile } from './routes/profile';
import { roles } from './routes/roles';
import { signInExperience } from './routes/sign-in-experience';
import { useTenantSettings } from './routes/tenant-settings';
Expand Down Expand Up @@ -62,7 +61,6 @@ export const useConsoleRoutes = () => {
{ path: steps.organizationInfo, element: <OrganizationInfo /> },
],
},
profile,
{ path: 'signing-keys', element: <SigningKeys /> },
isCloud && tenantSettings,
isCloud && customizeJwt
Expand Down
17 changes: 7 additions & 10 deletions packages/console/src/hooks/use-console-routes/routes/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import LinkEmailModal from '@/pages/Profile/containers/LinkEmailModal';
import VerificationCodeModal from '@/pages/Profile/containers/VerificationCodeModal';
import VerifyPasswordModal from '@/pages/Profile/containers/VerifyPasswordModal';

export const profile: RouteObject = {
path: 'profile',
children: [
{ index: true, element: <Profile /> },
{ path: 'verify-password', element: <VerifyPasswordModal /> },
{ path: 'change-password', element: <ChangePasswordModal /> },
{ path: 'link-email', element: <LinkEmailModal /> },
{ path: 'verification-code', element: <VerificationCodeModal /> },
],
};
export const profile: RouteObject[] = [
{ index: true, element: <Profile /> },
{ path: 'verify-password', element: <VerifyPasswordModal /> },
{ path: 'change-password', element: <ChangePasswordModal /> },
{ path: 'link-email', element: <LinkEmailModal /> },
{ path: 'verification-code', element: <VerificationCodeModal /> },
];
24 changes: 21 additions & 3 deletions packages/console/src/hooks/use-user-assets-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import type { UserAssetsServiceStatus } from '@logto/schemas';
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 shouldUseAdminApi = isCloud && isProfilePage;

const fetcher = useSwrFetcher<UserAssetsServiceStatus>(shouldUseAdminApi ? adminApi : api);
const { data, error } = useSWRImmutable<UserAssetsServiceStatus, RequestError>(
'api/user-assets/service-status'
`${shouldUseAdminApi ? 'me' : 'api'}/user-assets/service-status`,
fetcher
);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ function BasicUserInfoUpdateModal({ field, value: initialValue, isOpen, onClose
name="avatar"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploaderField name={name} value={value} onChange={onChange} />
<ImageUploaderField
name={name}
value={value}
uploadUrl="me/user-assets"
apiInstance={api}
onChange={onChange}
/>
)}
/>
) : (
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);
}
}
}
Loading

0 comments on commit 6615f63

Please sign in to comment.