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 all commits
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
13 changes: 6 additions & 7 deletions packages/console/src/cloud/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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 * as styles from './AppRoutes.module.scss';
import Main from './pages/Main';
Expand All @@ -19,12 +19,11 @@ 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
charIeszhao marked this conversation as resolved.
Show resolved Hide resolved
path={`${GlobalRoute.AcceptInvitation}/:invitationId`}
element={<AcceptInvitation />}
/>
<Route path={GlobalAnonymousRoute.Profile + '/*'} element={<Profile />} />
<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
3 changes: 2 additions & 1 deletion packages/console/src/containers/ConsoleContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ function ConsoleContent() {
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
const routeObjects = useConsoleRoutes();
const routes = useRoutes(routeObjects);
usePlausiblePageview(routeObjects);

// Use this hook here to make sure console listens to user tenant scope changes.
useTenantScopeListener();
usePlausiblePageview(routeObjects);

return (
<div className={styles.content}>
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
Expand Up @@ -8,11 +8,12 @@ import AppContent, { RedirectToFirstItem } from '@/containers/AppContent';
import ConsoleContent from '@/containers/ConsoleContent';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import TenantAccess from '@/containers/TenantAccess';
import { GlobalRoute } from '@/contexts/TenantsProvider';
import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider';
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 HandleSocialCallback from '@/pages/Profile/containers/HandleSocialCallback';
import Welcome from '@/pages/Welcome';
import { dropLeadingSlash } from '@/utils/url';
Expand All @@ -39,6 +40,9 @@ export function ConsoleRoutes() {
* console path to trigger the console routes.
*/}
{!isCloud && <Route path="/" element={<Navigate to={ossConsolePath} />} />}
{!isCloud && (
<Route path={ossConsolePath + GlobalAnonymousRoute.Profile + '/*'} element={<Profile />} />
)}
<Route path="/:tenantId" element={<Layout />}>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
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 @@ -21,6 +21,7 @@ import { isCloud } from '@/consts/env';
export enum GlobalAnonymousRoute {
Callback = '/callback',
SocialDemoCallback = '/social-demo-callback',
Profile = '/profile',
}

/**
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
@@ -1,18 +1,13 @@
import { type RouteObject } from 'react-router-dom';

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';

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[] = [
{ path: 'verify-password', element: <VerifyPasswordModal /> },
{ path: 'change-password', element: <ChangePasswordModal /> },
{ path: 'link-email', element: <LinkEmailModal /> },
{ path: 'verification-code', element: <VerificationCodeModal /> },
];
25 changes: 22 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,30 @@
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 { GlobalAnonymousRoute } 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 === GlobalAnonymousRoute.Profile ||
pathname.startsWith(GlobalAnonymousRoute.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