mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
refactor: fix test and components
This commit is contained in:
parent
c4e13ff525
commit
a3e3363b10
8 changed files with 52 additions and 62 deletions
|
@ -1,41 +1,17 @@
|
||||||
import { useLogto } from '@logto/react';
|
|
||||||
import { trySafe } from '@silverhand/essentials';
|
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { useHref } from 'react-router-dom';
|
|
||||||
|
|
||||||
import AppLoading from '@/components/AppLoading';
|
import AppLoading from '@/components/AppLoading';
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
|
|
||||||
function Redirect() {
|
function Redirect() {
|
||||||
const { getAccessToken, signIn } = useLogto();
|
const { navigateTenant, tenants, currentTenant } = useContext(TenantsContext);
|
||||||
const { navigateTenant, tenants, currentTenantId } = useContext(TenantsContext);
|
|
||||||
|
|
||||||
const tenant = tenants.find(({ id }) => id === currentTenantId);
|
|
||||||
const href = useHref(currentTenantId + '/callback');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validate = async (indicator: string) => {
|
if (!currentTenant) {
|
||||||
// Test fetching an access token for the current Tenant ID.
|
|
||||||
// If failed, it means the user finishes the first auth, ands still needs to auth again to
|
|
||||||
// fetch the full-scoped (with all available tenants) token.
|
|
||||||
if (await trySafe(getAccessToken(indicator))) {
|
|
||||||
navigateTenant(currentTenantId);
|
|
||||||
} else {
|
|
||||||
void signIn(new URL(href, window.location.origin).toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tenant) {
|
|
||||||
void validate(tenant.indicator);
|
|
||||||
}
|
|
||||||
}, [currentTenantId, getAccessToken, href, navigateTenant, signIn, tenant]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!tenant) {
|
|
||||||
/** Fallback to another available tenant instead of showing `Forbidden`. */
|
/** Fallback to another available tenant instead of showing `Forbidden`. */
|
||||||
navigateTenant(tenants[0]?.id ?? '');
|
navigateTenant(tenants[0]?.id ?? '');
|
||||||
}
|
}
|
||||||
}, [navigateTenant, tenant, tenants]);
|
}, [navigateTenant, currentTenant, tenants]);
|
||||||
|
|
||||||
return <AppLoading />;
|
return <AppLoading />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { defaultTenantId, ossConsolePath } from '@logto/schemas';
|
import { defaultTenantId, ossConsolePath } from '@logto/schemas';
|
||||||
|
import { conditionalArray } from '@silverhand/essentials';
|
||||||
|
|
||||||
import { CloudRoute } from '@/cloud/types';
|
import { CloudRoute } from '@/cloud/types';
|
||||||
|
|
||||||
|
@ -36,6 +37,9 @@ export const getUserTenantId = () => {
|
||||||
|
|
||||||
export const getBasename = () => (isCloud ? '/' + getUserTenantId() : ossConsolePath);
|
export const getBasename = () => (isCloud ? '/' + getUserTenantId() : ossConsolePath);
|
||||||
|
|
||||||
|
export const getCallbackUrl = (tenantId?: string) =>
|
||||||
|
new URL('/' + conditionalArray(tenantId, 'callback').join('/'), window.location.origin);
|
||||||
|
|
||||||
export const getSignOutRedirectPathname = () => (isCloud ? '/' : ossConsolePath);
|
export const getSignOutRedirectPathname = () => (isCloud ? '/' : ossConsolePath);
|
||||||
|
|
||||||
export const maxFreeTenantNumbers = 3;
|
export const maxFreeTenantNumbers = 3;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default function TenantSelector() {
|
||||||
appendTenant,
|
appendTenant,
|
||||||
currentTenant: currentTenantInfo,
|
currentTenant: currentTenantInfo,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
|
navigateTenant,
|
||||||
} = useContext(TenantsContext);
|
} = useContext(TenantsContext);
|
||||||
|
|
||||||
const isCreateButtonDisabled = useMemo(
|
const isCreateButtonDisabled = useMemo(
|
||||||
|
@ -75,7 +76,8 @@ export default function TenantSelector() {
|
||||||
key={id}
|
key={id}
|
||||||
className={styles.dropdownItem}
|
className={styles.dropdownItem}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(new URL(`/${id}`, window.location.origin).toString(), '_self');
|
navigateTenant(id);
|
||||||
|
setShowDropdown(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.dropdownName}>{name}</div>
|
<div className={styles.dropdownName}>{name}</div>
|
||||||
|
@ -110,7 +112,7 @@ export default function TenantSelector() {
|
||||||
onClose={async (tenant?: TenantInfo) => {
|
onClose={async (tenant?: TenantInfo) => {
|
||||||
if (tenant) {
|
if (tenant) {
|
||||||
appendTenant(tenant);
|
appendTenant(tenant);
|
||||||
window.location.assign(new URL(`/${tenant.id}`, window.location.origin).toString());
|
navigateTenant(tenant.id);
|
||||||
}
|
}
|
||||||
setShowCreateTenantModal(false);
|
setShowCreateTenantModal(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,16 +1,38 @@
|
||||||
import { useContext } from 'react';
|
import { useLogto } from '@logto/react';
|
||||||
|
import { type TenantInfo } from '@logto/schemas/lib/models/tenants.js';
|
||||||
|
import { trySafe } from '@silverhand/essentials';
|
||||||
|
import { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
import AppLoading from '@/components/AppLoading';
|
import AppLoading from '@/components/AppLoading';
|
||||||
|
import { getCallbackUrl } from '@/consts';
|
||||||
import { isCloud } from '@/consts/env';
|
import { isCloud } from '@/consts/env';
|
||||||
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
|
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
|
||||||
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import useTrackUserId from '@/hooks/use-track-user-id';
|
import useTrackUserId from '@/hooks/use-track-user-id';
|
||||||
import OnboardingApp from '@/onboarding/App';
|
import OnboardingApp from '@/onboarding/App';
|
||||||
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
|
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
|
||||||
import ConsoleApp from '@/pages/Main';
|
import ConsoleApp from '@/pages/Main';
|
||||||
|
|
||||||
function TenantAppContainer() {
|
function TenantAppContainer() {
|
||||||
|
const { getAccessToken, signIn } = useLogto();
|
||||||
const { userEndpoint } = useContext(AppEndpointsContext);
|
const { userEndpoint } = useContext(AppEndpointsContext);
|
||||||
const { isOnboarding, isLoaded } = useUserOnboardingData();
|
const { isOnboarding, isLoaded } = useUserOnboardingData();
|
||||||
|
const { currentTenant } = useContext(TenantsContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validate = async ({ indicator, id }: TenantInfo) => {
|
||||||
|
// Test fetching an access token for the current Tenant ID.
|
||||||
|
// If failed, it means the user finishes the first auth, ands still needs to auth again to
|
||||||
|
// fetch the full-scoped (with all available tenants) token.
|
||||||
|
if (!(await trySafe(getAccessToken(indicator)))) {
|
||||||
|
void signIn(getCallbackUrl(id).href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentTenant) {
|
||||||
|
void validate(currentTenant);
|
||||||
|
}
|
||||||
|
}, [currentTenant, getAccessToken, signIn]);
|
||||||
|
|
||||||
useTrackUserId();
|
useTrackUserId();
|
||||||
|
|
||||||
|
|
|
@ -76,19 +76,10 @@ function TenantsProvider({ children }: Props) {
|
||||||
const [isInitComplete, setIsInitComplete] = useState(!isCloud);
|
const [isInitComplete, setIsInitComplete] = useState(!isCloud);
|
||||||
const [currentTenantId, setCurrentTenantId] = useState(getUserTenantId());
|
const [currentTenantId, setCurrentTenantId] = useState(getUserTenantId());
|
||||||
|
|
||||||
const navigateTenant = useCallback((tenantId: string, options?: NavigateOptions) => {
|
const navigateTenant = useCallback((tenantId: string) => {
|
||||||
const params = [
|
// Use `window.open()` to force page reload since we use `basename` for the router
|
||||||
options?.state ?? {},
|
// which will not re-create the router instance when the URL changes.
|
||||||
'',
|
window.open(`/${tenantId}`, '_self');
|
||||||
new URL(`/${tenantId}`, window.location.origin).toString(),
|
|
||||||
] satisfies Parameters<typeof window.history.pushState>;
|
|
||||||
|
|
||||||
if (options?.replace) {
|
|
||||||
window.history.replaceState(...params);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.history.pushState(...params);
|
|
||||||
setCurrentTenantId(tenantId);
|
setCurrentTenantId(tenantId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -22,19 +22,28 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
|
||||||
|
|
||||||
function Main() {
|
function Main() {
|
||||||
const swrOptions = useSwrOptions();
|
const swrOptions = useSwrOptions();
|
||||||
|
|
||||||
const router = useMemo(
|
const router = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createBrowserRouter(
|
createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
<>
|
<Route path="/*">
|
||||||
<Route path="callback" element={<Callback />} />
|
<Route path="callback" element={<Callback />} />
|
||||||
<Route path="welcome" element={<Welcome />} />
|
<Route path="welcome" element={<Welcome />} />
|
||||||
<Route path="handle-social" element={<HandleSocialCallback />} />
|
<Route path="handle-social" element={<HandleSocialCallback />} />
|
||||||
<Route element={<AppContent />}>
|
<Route element={<AppContent />}>
|
||||||
<Route path="/*" element={<ConsoleContent />} />
|
<Route path="*" element={<ConsoleContent />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
|
// Currently we use `window.open()` to navigate between tenants so the `useMemo` hook
|
||||||
|
// can have no dependency and the router will be created anyway. Consider integrating the
|
||||||
|
// tenant ID into the router and remove basename here if we want to use `history.pushState()`
|
||||||
|
// to navigate.
|
||||||
|
//
|
||||||
|
// Caveat: To use `history.pushState()`, we'd better to create a browser router in the upper
|
||||||
|
// level of the component tree to make the tenant ID a part of the URL. Otherwise, we need
|
||||||
|
// to handle `popstate` event to update the tenant ID when the user navigates back.
|
||||||
{ basename: getBasename() }
|
{ basename: getBasename() }
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -101,20 +101,6 @@ function TenantBasicSettings() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* Redirect to the first tenant if the current tenant is deleted;
|
|
||||||
* Redirect to Cloud console landing page if there is no tenant.
|
|
||||||
*/
|
|
||||||
if (!tenants.some(({ id }) => id === currentTenantId)) {
|
|
||||||
window.location.assign(
|
|
||||||
tenants[0]?.id
|
|
||||||
? new URL(`/${tenants[0]?.id}`, window.location.origin).toString()
|
|
||||||
: new URL(window.location.origin).toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [currentTenantId, tenants]);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ describe('smoke testing for cloud', () => {
|
||||||
await expect(page).toClick('div[role=button][class$=item]');
|
await expect(page).toClick('div[role=button][class$=item]');
|
||||||
|
|
||||||
// Click the next button
|
// Click the next button
|
||||||
await expect(page).toClick('div[class$=actions] button:first-child');
|
await expect(page).toClick('div[class$=actions] button', { text: 'Next' });
|
||||||
|
|
||||||
// Wait for the next page to load
|
// Wait for the next page to load
|
||||||
await expect(page).toMatchElement('div[class$=config] div[class$=title]', {
|
await expect(page).toMatchElement('div[class$=config] div[class$=title]', {
|
||||||
|
@ -127,7 +127,7 @@ describe('smoke testing for cloud', () => {
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
const createTenantButton = await page.waitForSelector(
|
const createTenantButton = await page.waitForSelector(
|
||||||
'div[class$=ReactModalPortal] div[class$=dropdownContainer] > div[class$=dropdown] > div[class$=createTenantButton][role=button]:has(div)'
|
'div[class$=ReactModalPortal] div[class$=dropdownContainer] > div[class$=dropdown] > button[class$=createTenantButton]:has(div)'
|
||||||
);
|
);
|
||||||
await expect(createTenantButton).toMatchElement('div', { text: 'Create tenant' });
|
await expect(createTenantButton).toMatchElement('div', { text: 'Create tenant' });
|
||||||
await createTenantButton.click();
|
await createTenantButton.click();
|
||||||
|
|
Loading…
Add table
Reference in a new issue