Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(console): make profile a tenant independent page #5687

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`}
charIeszhao marked this conversation as resolved.
Show resolved Hide resolved
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
10 changes: 7 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,21 @@ import * as styles from './index.module.scss';

type Props = {
readonly className?: string;
/* eslint-disable react/boolean-prop-naming */
readonly hideTenantSelector?: boolean;
readonly hideTitle?: boolean;
/* eslint-enable react/boolean-prop-naming */
};

function Topbar({ className }: Props) {
function Topbar({ className, hideTenantSelector, hideTitle }: 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 && !hideTenantSelector && <TenantSelector />}
{!isCloud && !hideTitle && (
<>
<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
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]
[api, apiInstance, allowedMimeTypes, maxSize, onCompleted, t, uploadUrl]
);

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
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
Loading