mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(console): safely lazy load pages (#6332)
* refactor(console): safely lazy load pages * chore(console): use react-safe-lazy
This commit is contained in:
parent
ac40ef17d7
commit
c45a1a5ad4
25 changed files with 257 additions and 164 deletions
|
@ -15,6 +15,7 @@ module.exports = {
|
|||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/jsx-pascal-case': ['error', { ignore: ['__Internal__*'] }],
|
||||
'import/no-unused-modules': [
|
||||
'error',
|
||||
{
|
||||
|
@ -30,6 +31,8 @@ module.exports = {
|
|||
'**/assets/docs/guides/*/index.ts',
|
||||
'**/assets/docs/guides/*/components/**/*.tsx',
|
||||
'**/mdx-components*/*/index.tsx',
|
||||
'*.config.js',
|
||||
'*.config.ts',
|
||||
],
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
|
@ -49,12 +52,6 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.config.js', '*.config.ts', '*.d.ts'],
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
rules: {
|
||||
|
|
|
@ -107,6 +107,7 @@
|
|||
"react-modal": "^3.15.1",
|
||||
"react-paginate": "^8.1.3",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-safe-lazy": "^0.1.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-timer-hook": "^3.0.5",
|
||||
"recharts": "^2.1.13",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { lazy, Suspense } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { useOutletContext, useRoutes } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
|
@ -14,7 +15,7 @@ import { Skeleton } from './Sidebar';
|
|||
import useTenantScopeListener from './hooks';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const Sidebar = lazy(async () => import('./Sidebar'));
|
||||
const Sidebar = safeLazy(async () => import('./Sidebar'));
|
||||
|
||||
function ConsoleContent() {
|
||||
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { ossConsolePath } from '@logto/schemas';
|
||||
import { Suspense } from 'react';
|
||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { isCloud } from '@/consts/env';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import AppBoundary from '@/containers/AppBoundary';
|
||||
import AppContent, { RedirectToFirstItem } from '@/containers/AppContent';
|
||||
import ConsoleContent from '@/containers/ConsoleContent';
|
||||
|
@ -12,10 +15,13 @@ import { GlobalRoute } from '@/contexts/TenantsProvider';
|
|||
import useSwrOptions from '@/hooks/use-swr-options';
|
||||
import Callback from '@/pages/Callback';
|
||||
import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
|
||||
import Profile from '@/pages/Profile';
|
||||
import Welcome from '@/pages/Welcome';
|
||||
import { dropLeadingSlash } from '@/utils/url';
|
||||
|
||||
import { __Internal__ImportError } from './internal';
|
||||
|
||||
const Welcome = safeLazy(async () => import('@/pages/Welcome'));
|
||||
const Profile = safeLazy(async () => import('@/pages/Profile'));
|
||||
|
||||
function Layout() {
|
||||
const swrOptions = useSwrOptions();
|
||||
|
||||
|
@ -30,32 +36,37 @@ function Layout() {
|
|||
|
||||
export function ConsoleRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
{/**
|
||||
* OSS doesn't have a tenant concept nor root path handling component, but it may
|
||||
* navigate to the root path in frontend. In this case, we redirect it to the OSS
|
||||
* console path to trigger the console routes.
|
||||
*/}
|
||||
{!isCloud && <Route path="/" element={<Navigate to={ossConsolePath} />} />}
|
||||
<Route path="/:tenantId" element={<Layout />}>
|
||||
<Route path="callback" element={<Callback />} />
|
||||
<Route path="welcome" element={<Welcome />} />
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route path={dropLeadingSlash(GlobalRoute.Profile) + '/*'} element={<Profile />} />
|
||||
<Route element={<TenantAccess />}>
|
||||
{isCloud && (
|
||||
<Route
|
||||
path={dropLeadingSlash(GlobalRoute.CheckoutSuccessCallback)}
|
||||
element={<CheckoutSuccessCallback />}
|
||||
/>
|
||||
)}
|
||||
<Route element={<AppContent />}>
|
||||
<Route index element={<RedirectToFirstItem />} />
|
||||
<Route path="*" element={<ConsoleContent />} />
|
||||
<Suspense fallback={<AppLoading />}>
|
||||
<Routes>
|
||||
{/**
|
||||
* OSS doesn't have a tenant concept nor root path handling component, but it may
|
||||
* navigate to the root path in frontend. In this case, we redirect it to the OSS
|
||||
* console path to trigger the console routes.
|
||||
*/}
|
||||
{!isCloud && <Route path="/" element={<Navigate to={ossConsolePath} />} />}
|
||||
<Route path="/:tenantId" element={<Layout />}>
|
||||
<Route path="callback" element={<Callback />} />
|
||||
<Route path="welcome" element={<Welcome />} />
|
||||
{isDevFeaturesEnabled && (
|
||||
<Route path="__internal__/import-error" element={<__Internal__ImportError />} />
|
||||
)}
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route path={dropLeadingSlash(GlobalRoute.Profile) + '/*'} element={<Profile />} />
|
||||
<Route element={<TenantAccess />}>
|
||||
{isCloud && (
|
||||
<Route
|
||||
path={dropLeadingSlash(GlobalRoute.CheckoutSuccessCallback)}
|
||||
element={<CheckoutSuccessCallback />}
|
||||
/>
|
||||
)}
|
||||
<Route element={<AppContent />}>
|
||||
<Route index element={<RedirectToFirstItem />} />
|
||||
<Route path="*" element={<ConsoleContent />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
14
packages/console/src/containers/ConsoleRoutes/internal.ts
Normal file
14
packages/console/src/containers/ConsoleRoutes/internal.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
/**
|
||||
* An internal module that is used to test the lazy loading failure in the console. Normally, this
|
||||
* module should not involve any production code.
|
||||
*/
|
||||
export const __Internal__ImportError = safeLazy(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(
|
||||
/* @vite-ignore */ `${window.location.origin}/some-non-existing-path`
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return module;
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import { condArray } from '@silverhand/essentials';
|
||||
import { lazy, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { isCloud } from '@/consts/env';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
@ -20,9 +21,9 @@ import { useTenantSettings } from './routes/tenant-settings';
|
|||
import { users } from './routes/users';
|
||||
import { webhooks } from './routes/webhooks';
|
||||
|
||||
const Dashboard = lazy(async () => import('@/pages/Dashboard'));
|
||||
const GetStarted = lazy(async () => import('@/pages/GetStarted'));
|
||||
const SigningKeys = lazy(async () => import('@/pages/SigningKeys'));
|
||||
const Dashboard = safeLazy(async () => import('@/pages/Dashboard'));
|
||||
const GetStarted = safeLazy(async () => import('@/pages/GetStarted'));
|
||||
const SigningKeys = safeLazy(async () => import('@/pages/SigningKeys'));
|
||||
|
||||
export const useConsoleRoutes = () => {
|
||||
const tenantSettings = useTenantSettings();
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { ApiResourceDetailsTabs } from '@/consts';
|
||||
|
||||
const ApiResources = lazy(async () => import('@/pages/ApiResources'));
|
||||
const ApiResourceDetails = lazy(async () => import('@/pages/ApiResourceDetails'));
|
||||
const ApiResourcePermissions = lazy(
|
||||
const ApiResources = safeLazy(async () => import('@/pages/ApiResources'));
|
||||
const ApiResourceDetails = safeLazy(async () => import('@/pages/ApiResourceDetails'));
|
||||
const ApiResourcePermissions = safeLazy(
|
||||
async () => import('@/pages/ApiResourceDetails/ApiResourcePermissions')
|
||||
);
|
||||
const ApiResourceSettings = lazy(
|
||||
const ApiResourceSettings = safeLazy(
|
||||
async () => import('@/pages/ApiResourceDetails/ApiResourceSettings')
|
||||
);
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { ApplicationDetailsTabs } from '@/consts';
|
||||
|
||||
const Applications = lazy(async () => import('@/pages/Applications'));
|
||||
const ApplicationDetails = lazy(async () => import('@/pages/ApplicationDetails'));
|
||||
const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const Applications = safeLazy(async () => import('@/pages/Applications'));
|
||||
const ApplicationDetails = safeLazy(async () => import('@/pages/ApplicationDetails'));
|
||||
const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails'));
|
||||
|
||||
export const applications: RouteObject = {
|
||||
path: 'applications',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { lazy } from 'react';
|
||||
import { type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
const AuditLogs = lazy(async () => import('@/pages/AuditLogs'));
|
||||
const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const AuditLogs = safeLazy(async () => import('@/pages/AuditLogs'));
|
||||
const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails'));
|
||||
|
||||
export const auditLogs: RouteObject = {
|
||||
path: 'audit-logs',
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { ConnectorsTabs } from '@/consts';
|
||||
|
||||
const Connectors = lazy(async () => import('@/pages/Connectors'));
|
||||
const ConnectorDetails = lazy(async () => import('@/pages/ConnectorDetails'));
|
||||
const Connectors = safeLazy(async () => import('@/pages/Connectors'));
|
||||
const ConnectorDetails = safeLazy(async () => import('@/pages/ConnectorDetails'));
|
||||
|
||||
export const connectors = {
|
||||
path: 'connectors',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { lazy } from 'react';
|
||||
import { type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
const CustomizeJwt = lazy(async () => import('@/pages/CustomizeJwt'));
|
||||
const CustomizeJwtDetails = lazy(async () => import('@/pages/CustomizeJwtDetails'));
|
||||
const CustomizeJwt = safeLazy(async () => import('@/pages/CustomizeJwt'));
|
||||
const CustomizeJwtDetails = safeLazy(async () => import('@/pages/CustomizeJwtDetails'));
|
||||
|
||||
export const customizeJwt: RouteObject = {
|
||||
path: 'customize-jwt',
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { EnterpriseSsoDetailsTabs } from '@/consts/page-tabs';
|
||||
|
||||
const EnterpriseSso = lazy(async () => import('@/pages/EnterpriseSso'));
|
||||
const EnterpriseSsoDetails = lazy(async () => import('@/pages/EnterpriseSsoDetails'));
|
||||
const EnterpriseSso = safeLazy(async () => import('@/pages/EnterpriseSso'));
|
||||
const EnterpriseSsoDetails = safeLazy(async () => import('@/pages/EnterpriseSsoDetails'));
|
||||
|
||||
export const enterpriseSso: RouteObject = {
|
||||
path: 'enterprise-sso',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { lazy } from 'react';
|
||||
import { type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
const Mfa = lazy(async () => import('@/pages/Mfa'));
|
||||
const Mfa = safeLazy(async () => import('@/pages/Mfa'));
|
||||
|
||||
export const mfa: RouteObject = { path: 'mfa', element: <Mfa /> };
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts';
|
||||
|
||||
const OrganizationTemplate = lazy(async () => import('@/pages/OrganizationTemplate'));
|
||||
const OrganizationRoles = lazy(
|
||||
const OrganizationTemplate = safeLazy(async () => import('@/pages/OrganizationTemplate'));
|
||||
const OrganizationRoles = safeLazy(
|
||||
async () => import('@/pages/OrganizationTemplate/OrganizationRoles')
|
||||
);
|
||||
const OrganizationPermissions = lazy(
|
||||
const OrganizationPermissions = safeLazy(
|
||||
async () => import('@/pages/OrganizationTemplate/OrganizationPermissions')
|
||||
);
|
||||
const OrganizationRoleDetails = lazy(async () => import('@/pages/OrganizationRoleDetails'));
|
||||
const Permissions = lazy(async () => import('@/pages/OrganizationRoleDetails/Permissions'));
|
||||
const Settings = lazy(async () => import('@/pages/OrganizationRoleDetails/Settings'));
|
||||
const OrganizationRoleDetails = safeLazy(async () => import('@/pages/OrganizationRoleDetails'));
|
||||
const Permissions = safeLazy(async () => import('@/pages/OrganizationRoleDetails/Permissions'));
|
||||
const Settings = safeLazy(async () => import('@/pages/OrganizationRoleDetails/Settings'));
|
||||
|
||||
export const organizationTemplate: RouteObject[] = [
|
||||
{
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { condArray } from '@silverhand/essentials';
|
||||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { OrganizationDetailsTabs } from '@/pages/OrganizationDetails/types';
|
||||
|
||||
const Organizations = lazy(async () => import('@/pages/Organizations'));
|
||||
const OrganizationDetails = lazy(async () => import('@/pages/OrganizationDetails'));
|
||||
const MachineToMachine = lazy(async () => import('@/pages/OrganizationDetails/MachineToMachine'));
|
||||
const Members = lazy(async () => import('@/pages/OrganizationDetails/Members'));
|
||||
const Settings = lazy(async () => import('@/pages/OrganizationDetails/Settings'));
|
||||
const Organizations = safeLazy(async () => import('@/pages/Organizations'));
|
||||
const OrganizationDetails = safeLazy(async () => import('@/pages/OrganizationDetails'));
|
||||
const MachineToMachine = safeLazy(
|
||||
async () => import('@/pages/OrganizationDetails/MachineToMachine')
|
||||
);
|
||||
const Members = safeLazy(async () => import('@/pages/OrganizationDetails/Members'));
|
||||
const Settings = safeLazy(async () => import('@/pages/OrganizationDetails/Settings'));
|
||||
|
||||
export const organizations: RouteObject = {
|
||||
path: 'organizations',
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { lazy } from 'react';
|
||||
import { type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
const ChangePasswordModal = lazy(
|
||||
const ChangePasswordModal = safeLazy(
|
||||
async () => import('@/pages/Profile/containers/ChangePasswordModal')
|
||||
);
|
||||
const LinkEmailModal = lazy(async () => import('@/pages/Profile/containers/LinkEmailModal'));
|
||||
const VerificationCodeModal = lazy(
|
||||
const LinkEmailModal = safeLazy(async () => import('@/pages/Profile/containers/LinkEmailModal'));
|
||||
const VerificationCodeModal = safeLazy(
|
||||
async () => import('@/pages/Profile/containers/VerificationCodeModal')
|
||||
);
|
||||
const VerifyPasswordModal = lazy(
|
||||
const VerifyPasswordModal = safeLazy(
|
||||
async () => import('@/pages/Profile/containers/VerifyPasswordModal')
|
||||
);
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { RoleDetailsTabs } from '@/consts/page-tabs';
|
||||
|
||||
const Roles = lazy(async () => import('@/pages/Roles'));
|
||||
const RoleDetails = lazy(async () => import('@/pages/RoleDetails'));
|
||||
const RolePermissions = lazy(async () => import('@/pages/RoleDetails/RolePermissions'));
|
||||
const RoleSettings = lazy(async () => import('@/pages/RoleDetails/RoleSettings'));
|
||||
const RoleUsers = lazy(async () => import('@/pages/RoleDetails/RoleUsers'));
|
||||
const RoleApplications = lazy(async () => import('@/pages/RoleDetails/RoleApplications'));
|
||||
const Roles = safeLazy(async () => import('@/pages/Roles'));
|
||||
const RoleDetails = safeLazy(async () => import('@/pages/RoleDetails'));
|
||||
const RolePermissions = safeLazy(async () => import('@/pages/RoleDetails/RolePermissions'));
|
||||
const RoleSettings = safeLazy(async () => import('@/pages/RoleDetails/RoleSettings'));
|
||||
const RoleUsers = safeLazy(async () => import('@/pages/RoleDetails/RoleUsers'));
|
||||
const RoleApplications = safeLazy(async () => import('@/pages/RoleDetails/RoleApplications'));
|
||||
|
||||
export const roles: RouteObject = {
|
||||
path: 'roles',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { SignInExperienceTab } from '@/pages/SignInExperience/types';
|
||||
|
||||
const SignInExperience = lazy(async () => import('@/pages/SignInExperience'));
|
||||
const SignInExperience = safeLazy(async () => import('@/pages/SignInExperience'));
|
||||
|
||||
export const signInExperience: RouteObject = {
|
||||
path: 'sign-in-experience',
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
import { condArray } from '@silverhand/essentials';
|
||||
import { lazy, useContext, useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { TenantSettingsTabs } from '@/consts';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
const TenantSettings = lazy(async () => import('@/pages/TenantSettings'));
|
||||
const TenantBasicSettings = lazy(async () => import('@/pages/TenantSettings/TenantBasicSettings'));
|
||||
const TenantDomainSettings = lazy(
|
||||
const TenantSettings = safeLazy(async () => import('@/pages/TenantSettings'));
|
||||
const TenantBasicSettings = safeLazy(
|
||||
async () => import('@/pages/TenantSettings/TenantBasicSettings')
|
||||
);
|
||||
const TenantDomainSettings = safeLazy(
|
||||
async () => import('@/pages/TenantSettings/TenantDomainSettings')
|
||||
);
|
||||
const TenantMembers = lazy(async () => import('@/pages/TenantSettings/TenantMembers'));
|
||||
const Invitations = lazy(async () => import('@/pages/TenantSettings/TenantMembers/Invitations'));
|
||||
const Members = lazy(async () => import('@/pages/TenantSettings/TenantMembers/Members'));
|
||||
const BillingHistory = lazy(async () => import('@/pages/TenantSettings/BillingHistory'));
|
||||
const Subscription = lazy(async () => import('@/pages/TenantSettings/Subscription'));
|
||||
const TenantMembers = safeLazy(async () => import('@/pages/TenantSettings/TenantMembers'));
|
||||
const Invitations = safeLazy(
|
||||
async () => import('@/pages/TenantSettings/TenantMembers/Invitations')
|
||||
);
|
||||
const Members = safeLazy(async () => import('@/pages/TenantSettings/TenantMembers/Members'));
|
||||
const BillingHistory = safeLazy(async () => import('@/pages/TenantSettings/BillingHistory'));
|
||||
const Subscription = safeLazy(async () => import('@/pages/TenantSettings/Subscription'));
|
||||
|
||||
export const useTenantSettings = () => {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { UserDetailsTabs } from '@/consts/page-tabs';
|
||||
|
||||
const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const UserDetails = lazy(async () => import('@/pages/UserDetails'));
|
||||
const UserLogs = lazy(async () => import('@/pages/UserDetails/UserLogs'));
|
||||
const UserOrganizations = lazy(async () => import('@/pages/UserDetails/UserOrganizations'));
|
||||
const UserRoles = lazy(async () => import('@/pages/UserDetails/UserRoles'));
|
||||
const UserSettings = lazy(async () => import('@/pages/UserDetails/UserSettings'));
|
||||
const Users = lazy(async () => import('@/pages/Users'));
|
||||
const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const UserDetails = safeLazy(async () => import('@/pages/UserDetails'));
|
||||
const UserLogs = safeLazy(async () => import('@/pages/UserDetails/UserLogs'));
|
||||
const UserOrganizations = safeLazy(async () => import('@/pages/UserDetails/UserOrganizations'));
|
||||
const UserRoles = safeLazy(async () => import('@/pages/UserDetails/UserRoles'));
|
||||
const UserSettings = safeLazy(async () => import('@/pages/UserDetails/UserSettings'));
|
||||
const Users = safeLazy(async () => import('@/pages/Users'));
|
||||
|
||||
export const users: RouteObject = {
|
||||
path: 'users',
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { lazy } from 'react';
|
||||
import { Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { safeLazy } from 'react-safe-lazy';
|
||||
|
||||
import { WebhookDetailsTabs } from '@/consts';
|
||||
|
||||
const WebhookDetails = lazy(async () => import('@/pages/WebhookDetails'));
|
||||
const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const WebhookSettings = lazy(async () => import('@/pages/WebhookDetails/WebhookSettings'));
|
||||
const WebhookLogs = lazy(async () => import('@/pages/WebhookDetails/WebhookLogs'));
|
||||
const Webhooks = lazy(async () => import('@/pages/Webhooks'));
|
||||
const WebhookDetails = safeLazy(async () => import('@/pages/WebhookDetails'));
|
||||
const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails'));
|
||||
const WebhookSettings = safeLazy(async () => import('@/pages/WebhookDetails/WebhookSettings'));
|
||||
const WebhookLogs = safeLazy(async () => import('@/pages/WebhookDetails/WebhookLogs'));
|
||||
const Webhooks = safeLazy(async () => import('@/pages/Webhooks'));
|
||||
|
||||
export const webhooks: RouteObject = {
|
||||
path: 'webhooks',
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { appendPath } from '@silverhand/essentials';
|
||||
|
||||
import ExpectConsole from '#src/ui-helpers/expect-console.js';
|
||||
import { Trace } from '#src/ui-helpers/trace.js';
|
||||
import { devFeatureTest } from '#src/utils.js';
|
||||
|
||||
describe('error handling', () => {
|
||||
const trace = new Trace();
|
||||
|
||||
devFeatureTest.it('should handle dynamic import errors', async () => {
|
||||
const expectConsole = new ExpectConsole(await browser.newPage());
|
||||
const path = appendPath(expectConsole.options.endpoint, 'console/__internal__/import-error');
|
||||
|
||||
trace.reset(expectConsole.page);
|
||||
await trace.start();
|
||||
await expectConsole.navigateTo(path);
|
||||
await trace.stop();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
const traceData: { traceEvents: any[] } = await trace.read();
|
||||
|
||||
const documentLoads = traceData.traceEvents.filter((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = item?.args?.data ?? {};
|
||||
return (
|
||||
data.resourceType === 'Document' && data.requestMethod === 'GET' && data.url === path.href
|
||||
);
|
||||
});
|
||||
|
||||
// Reloaded once
|
||||
expect(documentLoads).toHaveLength(2);
|
||||
|
||||
// Show the error message
|
||||
await Promise.all([
|
||||
expectConsole.toMatchElement('label', {
|
||||
text: 'Oops! Something went wrong.',
|
||||
}),
|
||||
expectConsole.toMatchElement('span', {
|
||||
text: 'Failed to fetch dynamically imported module',
|
||||
}),
|
||||
expectConsole.toMatchElement('button', {
|
||||
text: 'Try again',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,14 +1,10 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { demoAppApplicationId, fullSignInExperienceGuard } from '@logto/schemas';
|
||||
import { type Page } from 'puppeteer';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { demoAppUrl } from '#src/constants.js';
|
||||
import { OrganizationApiTest } from '#src/helpers/organization.js';
|
||||
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
||||
import { Trace } from '#src/ui-helpers/trace.js';
|
||||
|
||||
const ssrDataGuard = z.object({
|
||||
signInExperience: z.object({
|
||||
|
@ -22,53 +18,6 @@ const ssrDataGuard = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
class Trace {
|
||||
protected tracePath?: string;
|
||||
|
||||
constructor(protected page?: Page) {}
|
||||
|
||||
async start() {
|
||||
if (this.tracePath) {
|
||||
throw new Error('Trace already started');
|
||||
}
|
||||
|
||||
if (!this.page) {
|
||||
throw new Error('Page not set');
|
||||
}
|
||||
|
||||
const traceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'trace-'));
|
||||
this.tracePath = path.join(traceDirectory, 'trace.json');
|
||||
await this.page.tracing.start({ path: this.tracePath, categories: ['devtools.timeline'] });
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.page) {
|
||||
throw new Error('Page not set');
|
||||
}
|
||||
|
||||
return this.page.tracing.stop();
|
||||
}
|
||||
|
||||
async read() {
|
||||
if (!this.tracePath) {
|
||||
throw new Error('Trace not started');
|
||||
}
|
||||
|
||||
return JSON.parse(await fs.readFile(this.tracePath, 'utf8'));
|
||||
}
|
||||
|
||||
reset(page: Page) {
|
||||
this.page = page;
|
||||
this.tracePath = undefined;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.tracePath) {
|
||||
await fs.unlink(this.tracePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('server-side rendering', () => {
|
||||
const trace = new Trace();
|
||||
const expectTraceNotToHaveWellKnownEndpoints = async () => {
|
||||
|
|
54
packages/integration-tests/src/ui-helpers/trace.ts
Normal file
54
packages/integration-tests/src/ui-helpers/trace.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { type Page } from 'puppeteer';
|
||||
|
||||
export class Trace {
|
||||
protected tracePath?: string;
|
||||
|
||||
constructor(protected page?: Page) {}
|
||||
|
||||
async start() {
|
||||
if (this.tracePath) {
|
||||
throw new Error('Trace already started');
|
||||
}
|
||||
|
||||
if (!this.page) {
|
||||
throw new Error('Page not set');
|
||||
}
|
||||
|
||||
const traceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'trace-'));
|
||||
this.tracePath = path.join(traceDirectory, 'trace.json');
|
||||
await this.page.tracing.start({ path: this.tracePath, categories: ['devtools.timeline'] });
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (!this.page) {
|
||||
throw new Error('Page not set');
|
||||
}
|
||||
|
||||
console.log('Trace captured at', this.tracePath);
|
||||
return this.page.tracing.stop();
|
||||
}
|
||||
|
||||
async read() {
|
||||
if (!this.tracePath) {
|
||||
throw new Error('Trace not started');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(await fs.readFile(this.tracePath, 'utf8'));
|
||||
}
|
||||
|
||||
reset(page: Page) {
|
||||
this.page = page;
|
||||
this.tracePath = undefined;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
if (this.tracePath) {
|
||||
await fs.unlink(this.tracePath);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3100,6 +3100,9 @@ importers:
|
|||
react-router-dom:
|
||||
specifier: ^6.25.1
|
||||
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-safe-lazy:
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0(react@18.3.1)
|
||||
react-syntax-highlighter:
|
||||
specifier: ^15.5.0
|
||||
version: 15.5.0(react@18.3.1)
|
||||
|
@ -11700,6 +11703,11 @@ packages:
|
|||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
react-safe-lazy@0.1.0:
|
||||
resolution: {integrity: sha512-CZSaQHlNVG8OuSRaLkBGe5cP+qjZtW+fJA/PRNTyCIVkH3F7GQO0aBu+jIrm2PFrTVs4xjSvuDFVyzlX6OL0oQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0
|
||||
|
||||
react-side-effect@2.1.2:
|
||||
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
|
||||
peerDependencies:
|
||||
|
@ -23042,6 +23050,10 @@ snapshots:
|
|||
'@remix-run/router': 1.18.0
|
||||
react: 18.3.1
|
||||
|
||||
react-safe-lazy@0.1.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
react-side-effect@2.1.2(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
|
Loading…
Reference in a new issue