0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): refactor console routes (#3294)

This commit is contained in:
Xiao Yijun 2023-03-06 20:36:24 +08:00 committed by GitHub
parent 8caa752106
commit ec65d46826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 248 additions and 226 deletions

View file

@ -4,7 +4,6 @@ import Callback from '@cloud/pages/Callback';
import * as styles from './App.module.scss';
import Main from './pages/Main';
import Onboard from './pages/Onboard';
import { CloudRoute } from './types';
const App = () => {
@ -12,7 +11,6 @@ const App = () => {
<BrowserRouter>
<div className={styles.app}>
<Routes>
<Route path={`/${CloudRoute.Onboard}/*`} element={<Onboard />} />
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
<Route path="/*" element={<Main />} />

View file

@ -7,10 +7,12 @@ import AirPlay from '@/assets/images/air-play.svg';
import GetStarted from '@/assets/images/get-started.svg';
import ActionBar from '@/cloud/components/ActionBar';
import Reservation from '@/cloud/components/Reservation';
import useUserOnboardingData from '@/cloud/hooks/use-user-onboarding-data';
import * as pageLayout from '@/cloud/scss/layout.module.scss';
import Button from '@/components/Button';
import Divider from '@/components/Divider';
import OverlayScrollbar from '@/components/OverlayScrollbar';
import { getBasename } from '@/consts';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import { OnboardPage } from '../../types';
@ -20,11 +22,13 @@ import * as styles from './index.module.scss';
const Congrats = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { userEndpoint } = useContext(AppEndpointsContext);
const { update } = useUserOnboardingData();
const navigate = useNavigate();
const enterAdminConsole = () => {
navigate('/');
const enterAdminConsole = async () => {
await update({ hasOnboard: true });
navigate(getBasename());
};
const handleBack = () => {

View file

@ -3,7 +3,6 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import useUserOnboardingData from '@/cloud/hooks/use-user-onboarding-data';
import { OnboardPage } from '@/cloud/types';
import { getOnboardPagePathname } from '@/cloud/utils';
import Topbar from '@/containers/AppLayout/components/Topbar';
import NotFound from '@/pages/NotFound';
import About from '../About';
@ -26,7 +25,6 @@ const Onboard = () => {
return (
<div className={styles.onBoard}>
<Topbar />
<Routes>
<Route index element={<Navigate replace to={welcomePathname} />} />
<Route path={OnboardPage.Welcome} element={<Welcome />} />

View file

@ -62,6 +62,7 @@ export type Questionnaire = z.infer<typeof questionnaireGuard>;
export const userOnboardingDataGuard = z.object({
questionnaire: questionnaireGuard.optional(),
hasOnboard: z.boolean().optional(),
});
export type UserOnboardingData = z.infer<typeof userOnboardingDataGuard>;

View file

@ -1,17 +1,13 @@
@use '@/scss/underscore' as _;
@use '@logto/core-kit/scss/console-themes' as themes;
.content {
flex-grow: 1;
.app {
position: absolute;
inset: 0;
display: flex;
overflow: hidden;
flex-direction: column;
}
.main {
flex-grow: 1;
padding: 0 _.unit(2);
overflow-y: scroll;
> * {
@include _.main-content-width;
}
.topbarShadow {
box-shadow: var(--shadow-2);
}

View file

@ -1,18 +1,84 @@
import { Outlet, useOutletContext } from 'react-router-dom';
import { LogtoClientError, LogtoError, useLogto } from '@logto/react';
import { conditional } from '@silverhand/essentials';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useHref, useLocation, useNavigate } from 'react-router-dom';
import type { AppLayoutOutletContext } from '../AppLayout/types';
import Sidebar from './Sidebar';
import useUserOnboardingData from '@/cloud/hooks/use-user-onboarding-data';
import { OnboardPage } from '@/cloud/types';
import { getOnboardPagePathname } from '@/cloud/utils';
import AppError from '@/components/AppError';
import AppLoading from '@/components/AppLoading';
import SessionExpired from '@/components/SessionExpired';
import { isCloud } from '@/consts/cloud';
import useConfigs from '@/hooks/use-configs';
import useScroll from '@/hooks/use-scroll';
import useUserPreferences from '@/hooks/use-user-preferences';
import { getPath } from '../ConsoleContent/Sidebar';
import { useSidebarMenuItems } from '../ConsoleContent/Sidebar/hook';
import Topbar from './components/Topbar';
import * as styles from './index.module.scss';
import { AppContentOutletContext } from './types';
const AppContent = () => {
const { scrollableContent } = useOutletContext<AppLayoutOutletContext>();
const { isAuthenticated, isLoading: isLogtoLoading, error, signIn } = useLogto();
const href = useHref('/callback');
const { isLoading: isPreferencesLoading } = useUserPreferences();
const { isLoading: isConfigsLoading } = useConfigs();
const {
data: { hasOnboard },
isLoading: isOnboardingDataLoading,
isLoaded: isOnboardingDataLoaded,
} = useUserOnboardingData();
const isLoading =
isPreferencesLoading || isConfigsLoading || (isCloud && isOnboardingDataLoading);
const isOnboarding = isCloud && isOnboardingDataLoaded && !hasOnboard;
const location = useLocation();
const navigate = useNavigate();
const { firstItem } = useSidebarMenuItems();
const scrollableContent = useRef<HTMLDivElement>(null);
const { scrollTop } = useScroll(scrollableContent.current);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
useEffect(() => {
if (!isAuthenticated && !isLogtoLoading) {
void signIn(new URL(href, window.location.origin).toString());
}
}, [href, isAuthenticated, isLogtoLoading, signIn]);
useEffect(() => {
// Navigate to the first menu item after configs are loaded.
if (!isLoading && location.pathname === '/') {
navigate(
isOnboarding ? getOnboardPagePathname(OnboardPage.Welcome) : getPath(firstItem?.title ?? '')
);
}
}, [firstItem?.title, hasOnboard, isLoading, isOnboarding, location.pathname, navigate]);
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired error={error} callbackHref={href} />;
}
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {
return <AppError errorMessage={t('errors.insecure_contexts')} callStack={error.stack} />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (!isAuthenticated || isLoading) {
return <AppLoading />;
}
return (
<div className={styles.content}>
<Sidebar />
<div ref={scrollableContent} className={styles.main}>
<Outlet />
</div>
<div className={styles.app}>
<Topbar className={conditional(scrollTop && styles.topbarShadow)} />
<Outlet context={{ scrollableContent } satisfies AppContentOutletContext} />
</div>
);
};

View file

@ -1,5 +1,5 @@
import type { RefObject } from 'react';
export type AppLayoutOutletContext = {
export type AppContentOutletContext = {
scrollableContent: RefObject<HTMLDivElement>;
};

View file

@ -1,13 +0,0 @@
@use '@/scss/underscore' as _;
@use '@logto/core-kit/scss/console-themes' as themes;
.app {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
}
.topbarShadow {
box-shadow: var(--shadow-2);
}

View file

@ -1,71 +0,0 @@
import { LogtoClientError, LogtoError, useLogto } from '@logto/react';
import { conditional } from '@silverhand/essentials';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useHref, useLocation, useNavigate } from 'react-router-dom';
import AppError from '@/components/AppError';
import AppLoading from '@/components/AppLoading';
import SessionExpired from '@/components/SessionExpired';
import useConfigs from '@/hooks/use-configs';
import useScroll from '@/hooks/use-scroll';
import useUserPreferences from '@/hooks/use-user-preferences';
import { getPath } from '../AppContent/Sidebar';
import { useSidebarMenuItems } from '../AppContent/Sidebar/hook';
import Topbar from './components/Topbar';
import * as styles from './index.module.scss';
import { AppLayoutOutletContext } from './types';
const AppLayout = () => {
const { isAuthenticated, isLoading: isLogtoLoading, error, signIn } = useLogto();
const href = useHref('/callback');
const { isLoading: isPreferencesLoading } = useUserPreferences();
const { isLoading: isConfigsLoading } = useConfigs();
const isLoading = isPreferencesLoading || isConfigsLoading;
const location = useLocation();
const navigate = useNavigate();
const { firstItem } = useSidebarMenuItems();
const scrollableContent = useRef<HTMLDivElement>(null);
const { scrollTop } = useScroll(scrollableContent.current);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
useEffect(() => {
if (!isAuthenticated && !isLogtoLoading) {
void signIn(new URL(href, window.location.origin).toString());
}
}, [href, isAuthenticated, isLogtoLoading, signIn]);
useEffect(() => {
// Navigate to the first menu item after configs are loaded.
if (!isLoading && location.pathname === '/') {
navigate(getPath(firstItem?.title ?? ''));
}
}, [firstItem?.title, isLoading, location.pathname, navigate]);
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired error={error} callbackHref={href} />;
}
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {
return <AppError errorMessage={t('errors.insecure_contexts')} callStack={error.stack} />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (!isAuthenticated || isLoading) {
return <AppLoading />;
}
return (
<div className={styles.app}>
<Topbar className={conditional(scrollTop && styles.topbarShadow)} />
<Outlet context={{ scrollableContent } satisfies AppLayoutOutletContext} />
</div>
);
};
export default AppLayout;

View file

@ -0,0 +1,17 @@
@use '@/scss/underscore' as _;
.content {
flex-grow: 1;
display: flex;
overflow: hidden;
}
.main {
flex-grow: 1;
padding: 0 _.unit(2);
overflow-y: scroll;
> * {
@include _.main-content-width;
}
}

View file

@ -0,0 +1,120 @@
import { Navigate, Route, Routes, useOutletContext } from 'react-router-dom';
import {
ApiResourceDetailsTabs,
SignInExperiencePage,
ConnectorsTabs,
UserDetailsTabs,
RoleDetailsTabs,
} from '@/consts';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
import ApiResources from '@/pages/ApiResources';
import ApplicationDetails from '@/pages/ApplicationDetails';
import Applications from '@/pages/Applications';
import AuditLogDetails from '@/pages/AuditLogDetails';
import AuditLogs from '@/pages/AuditLogs';
import ConnectorDetails from '@/pages/ConnectorDetails';
import Connectors from '@/pages/Connectors';
import Dashboard from '@/pages/Dashboard';
import GetStarted from '@/pages/GetStarted';
import NotFound from '@/pages/NotFound';
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';
import RoleDetails from '@/pages/RoleDetails';
import RolePermissions from '@/pages/RoleDetails/RolePermissions';
import RoleSettings from '@/pages/RoleDetails/RoleSettings';
import RoleUsers from '@/pages/RoleDetails/RoleUsers';
import Roles from '@/pages/Roles';
import SignInExperience from '@/pages/SignInExperience';
import UserDetails from '@/pages/UserDetails';
import UserLogs from '@/pages/UserDetails/UserLogs';
import UserRoles from '@/pages/UserDetails/UserRoles';
import UserSettings from '@/pages/UserDetails/UserSettings';
import Users from '@/pages/Users';
import type { AppContentOutletContext } from '../AppContent/types';
import Sidebar from './Sidebar';
import * as styles from './index.module.scss';
const ConsoleContent = () => {
const { scrollableContent } = useOutletContext<AppContentOutletContext>();
return (
<div className={styles.content}>
<Sidebar />
<div ref={scrollableContent} className={styles.main}>
<Routes>
<Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path="create" element={<Applications />} />
<Route path=":id" element={<ApplicationDetails />} />
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path="create" element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />}>
<Route index element={<Navigate replace to={ApiResourceDetailsTabs.Settings} />} />
<Route path={ApiResourceDetailsTabs.Settings} element={<ApiResourceSettings />} />
<Route
path={ApiResourceDetailsTabs.Permissions}
element={<ApiResourcePermissions />}
/>
</Route>
</Route>
<Route path="sign-in-experience">
<Route index element={<Navigate replace to={SignInExperiencePage.BrandingTab} />} />
<Route path=":tab" element={<SignInExperience />} />
</Route>
<Route path="connectors">
<Route index element={<Navigate replace to={ConnectorsTabs.Passwordless} />} />
<Route path=":tab" element={<Connectors />} />
<Route path=":tab/create/:createType" element={<Connectors />} />
<Route path=":tab/:connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path="create" element={<Users />} />
<Route path=":id" element={<UserDetails />}>
<Route index element={<Navigate replace to={UserDetailsTabs.Settings} />} />
<Route path={UserDetailsTabs.Settings} element={<UserSettings />} />
<Route path={UserDetailsTabs.Roles} element={<UserRoles />} />
<Route path={UserDetailsTabs.Logs} element={<UserLogs />} />
</Route>
<Route path={`:id/${UserDetailsTabs.Logs}/:logId`} element={<AuditLogDetails />} />
</Route>
<Route path="audit-logs">
<Route index element={<AuditLogs />} />
<Route path=":logId" element={<AuditLogDetails />} />
</Route>
<Route path="roles">
<Route index element={<Roles />} />
<Route path="create" element={<Roles />} />
<Route path=":id" element={<RoleDetails />}>
<Route index element={<Navigate replace to={RoleDetailsTabs.Settings} />} />
<Route path={RoleDetailsTabs.Settings} element={<RoleSettings />} />
<Route path={RoleDetailsTabs.Permissions} element={<RolePermissions />} />
<Route path={RoleDetailsTabs.Users} element={<RoleUsers />} />
</Route>
</Route>
<Route path="profile">
<Route index element={<Profile />} />
<Route path="verify-password" element={<VerifyPasswordModal />} />
<Route path="change-password" element={<ChangePasswordModal />} />
<Route path="link-email" element={<LinkEmailModal />} />
<Route path="verification-code" element={<VerificationCodeModal />} />
</Route>
</Routes>
</div>
</div>
);
};
export default ConsoleContent;

View file

@ -2,63 +2,37 @@ import { useContext } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import useUserOnboardingData from '@/cloud/hooks/use-user-onboarding-data';
import Onboard from '@/cloud/pages/Onboard';
import { CloudRoute } from '@/cloud/types';
import AppLoading from '@/components/AppLoading';
import Toast from '@/components/Toast';
import {
ApiResourceDetailsTabs,
ConnectorsTabs,
getBasename,
RoleDetailsTabs,
SignInExperiencePage,
UserDetailsTabs,
} from '@/consts';
import { getBasename } from '@/consts';
import { isCloud } from '@/consts/cloud';
import AppBoundary from '@/containers/AppBoundary';
import AppContent from '@/containers/AppContent';
import AppLayout from '@/containers/AppLayout';
import ConsoleContent from '@/containers/ConsoleContent';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import useSwrOptions from '@/hooks/use-swr-options';
import ApiResourceDetails from '@/pages/ApiResourceDetails';
import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings';
import ApiResources from '@/pages/ApiResources';
import ApplicationDetails from '@/pages/ApplicationDetails';
import Applications from '@/pages/Applications';
import AuditLogDetails from '@/pages/AuditLogDetails';
import AuditLogs from '@/pages/AuditLogs';
import Callback from '@/pages/Callback';
import ConnectorDetails from '@/pages/ConnectorDetails';
import Connectors from '@/pages/Connectors';
import Dashboard from '@/pages/Dashboard';
import GetStarted from '@/pages/GetStarted';
import NotFound from '@/pages/NotFound';
import Profile from '@/pages/Profile';
import RoleDetails from '@/pages/RoleDetails';
import RolePermissions from '@/pages/RoleDetails/RolePermissions';
import RoleSettings from '@/pages/RoleDetails/RoleSettings';
import RoleUsers from '@/pages/RoleDetails/RoleUsers';
import Roles from '@/pages/Roles';
import SignInExperience from '@/pages/SignInExperience';
import UserDetails from '@/pages/UserDetails';
import UserLogs from '@/pages/UserDetails/UserLogs';
import UserRoles from '@/pages/UserDetails/UserRoles';
import UserSettings from '@/pages/UserDetails/UserSettings';
import Users from '@/pages/Users';
import Welcome from '@/pages/Welcome';
import ChangePasswordModal from '../Profile/containers/ChangePasswordModal';
import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
import LinkEmailModal from '../Profile/containers/LinkEmailModal';
import VerificationCodeModal from '../Profile/containers/VerificationCodeModal';
import VerifyPasswordModal from '../Profile/containers/VerifyPasswordModal';
const Main = () => {
const swrOptions = useSwrOptions();
const { userEndpoint } = useContext(AppEndpointsContext);
const {
data: { hasOnboard },
isLoaded,
} = useUserOnboardingData();
if (!userEndpoint) {
if (!userEndpoint || (isCloud && !isLoaded)) {
return <AppLoading />;
}
const isOnboarding = isCloud && !hasOnboard;
return (
<BrowserRouter basename={getBasename()}>
<SWRConfig value={swrOptions}>
@ -68,83 +42,15 @@ const Main = () => {
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route element={<AppLayout />}>
<Route element={<AppContent />}>
<Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path="create" element={<Applications />} />
<Route path=":id" element={<ApplicationDetails />} />
<Route element={<AppContent />}>
{isOnboarding ? (
<Route>
<Route index element={<Navigate replace to={`/${CloudRoute.Onboard}`} />} />
<Route path={`/${CloudRoute.Onboard}/*`} element={<Onboard />} />
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path="create" element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />}>
<Route
index
element={<Navigate replace to={ApiResourceDetailsTabs.Settings} />}
/>
<Route
path={ApiResourceDetailsTabs.Settings}
element={<ApiResourceSettings />}
/>
<Route
path={ApiResourceDetailsTabs.Permissions}
element={<ApiResourcePermissions />}
/>
</Route>
</Route>
<Route path="sign-in-experience">
<Route
index
element={<Navigate replace to={SignInExperiencePage.BrandingTab} />}
/>
<Route path=":tab" element={<SignInExperience />} />
</Route>
<Route path="connectors">
<Route index element={<Navigate replace to={ConnectorsTabs.Passwordless} />} />
<Route path=":tab" element={<Connectors />} />
<Route path=":tab/create/:createType" element={<Connectors />} />
<Route path=":tab/:connectorId" element={<ConnectorDetails />} />
</Route>
<Route path="users">
<Route index element={<Users />} />
<Route path="create" element={<Users />} />
<Route path=":id" element={<UserDetails />}>
<Route index element={<Navigate replace to={UserDetailsTabs.Settings} />} />
<Route path={UserDetailsTabs.Settings} element={<UserSettings />} />
<Route path={UserDetailsTabs.Roles} element={<UserRoles />} />
<Route path={UserDetailsTabs.Logs} element={<UserLogs />} />
</Route>
<Route
path={`:id/${UserDetailsTabs.Logs}/:logId`}
element={<AuditLogDetails />}
/>
</Route>
<Route path="audit-logs">
<Route index element={<AuditLogs />} />
<Route path=":logId" element={<AuditLogDetails />} />
</Route>
<Route path="roles">
<Route index element={<Roles />} />
<Route path="create" element={<Roles />} />
<Route path=":id" element={<RoleDetails />}>
<Route index element={<Navigate replace to={RoleDetailsTabs.Settings} />} />
<Route path={RoleDetailsTabs.Settings} element={<RoleSettings />} />
<Route path={RoleDetailsTabs.Permissions} element={<RolePermissions />} />
<Route path={RoleDetailsTabs.Users} element={<RoleUsers />} />
</Route>
</Route>
<Route path="profile">
<Route index element={<Profile />} />
<Route path="verify-password" element={<VerifyPasswordModal />} />
<Route path="change-password" element={<ChangePasswordModal />} />
<Route path="link-email" element={<LinkEmailModal />} />
<Route path="verification-code" element={<VerificationCodeModal />} />
</Route>
</Route>
) : (
<Route path="/*" element={<ConsoleContent />} />
)}
</Route>
</Routes>
</AppBoundary>