Compare commits

...
Author SHA1 Message Date
Ludy bce4d2bd04 Merge branch 'main' into fast_setting_menu_20260810 2026-08-13 00:19:40 +02:00
Ludy87 384e200663 perf(settings): reduce config modal render time 2026-08-10 11:53:16 +02:00
6 changed files with 233 additions and 55 deletions
@@ -1,4 +1,5 @@
import React, {
Suspense,
useMemo,
useState,
useEffect,
@@ -270,6 +271,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
withCloseButton={false}
zIndex={Z_INDEX_CONFIG_MODAL}
overlayProps={{ opacity: 0.35, blur: 2 }}
transitionProps={{ duration: 0 }}
padding={0}
fullScreen={isMobile}
styles={{ content: { overflowY: "hidden", overscrollBehavior: "none" } }}
@@ -429,7 +431,11 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({
</ActionIcon>
</Group>
</div>
<div className="modal-body">{activeComponent}</div>
<div className="modal-body">
<Suspense fallback={<div className="modal-section-loading" />}>
{activeComponent}
</Suspense>
</div>
</div>
</div>
</div>
@@ -1,3 +1,4 @@
import { Loader } from "@mantine/core";
import { Suspense, lazy, useEffect, useState } from "react";
import type {
ConfigNavSection,
@@ -5,11 +6,32 @@ import type {
} from "@app/components/shared/config/types";
// AppConfigModal pulls in the entire settings UI tree (admin sections,
// account, supabase auth flows, etc.). We defer loading until the user first
// opens the modal, then keep it mounted so the close animation runs.
const AppConfigModal = lazy(
() => import("@app/components/shared/AppConfigModal"),
);
// account, supabase auth flows, etc.). Keep the import deferred, but share the
// promise so callers can preload it before the modal is opened.
let appConfigModalPromise: Promise<
typeof import("@app/components/shared/AppConfigModal")
> | null = null;
let generalSectionPromise: Promise<
typeof import("@app/components/shared/config/configSections/GeneralSection")
> | null = null;
const loadAppConfigModal = () => {
appConfigModalPromise ??= import("@app/components/shared/AppConfigModal");
return appConfigModalPromise;
};
const loadGeneralSection = () => {
generalSectionPromise ??=
import("@app/components/shared/config/configSections/GeneralSection");
return generalSectionPromise;
};
const AppConfigModal = lazy(loadAppConfigModal);
export function preloadAppConfigModal() {
void loadAppConfigModal();
void loadGeneralSection();
}
interface AppConfigModalLazyProps {
opened: boolean;
@@ -34,12 +56,42 @@ export default function AppConfigModalLazy({
}: AppConfigModalLazyProps) {
const [shouldMount, setShouldMount] = useState(false);
// Settings is opened frequently from the persistent sidebar. Warm the two
// chunks needed for the initial screen after the main page has painted, so
// the first click does not have to wait for module evaluation.
useEffect(() => {
if (opened) setShouldMount(true);
const timer = window.setTimeout(preloadAppConfigModal, 500);
return () => window.clearTimeout(timer);
}, []);
useEffect(() => {
// Unmount the settings tree immediately on close. Keeping the hidden tree
// alive makes Mantine's overlay transition re-layout the entire settings
// page and blocks the close click on large admin builds.
setShouldMount(opened);
}, [opened]);
return (
<Suspense fallback={null}>
<Suspense
fallback={
opened ? (
<div
role="status"
aria-label="Loading settings"
style={{
position: "fixed",
inset: 0,
display: "grid",
placeItems: "center",
zIndex: 1300,
background: "var(--c-overlay, rgba(0, 0, 0, 0.35))",
}}
>
<Loader size="sm" />
</div>
) : null
}
>
{shouldMount && (
<AppConfigModal
opened={opened}
@@ -1,18 +1,39 @@
import React from "react";
import React, { lazy } from "react";
import { useTranslation } from "react-i18next";
import HotkeysSection from "@app/components/shared/config/configSections/HotkeysSection";
import GeneralSection from "@app/components/shared/config/configSections/GeneralSection";
import HelpSection from "@app/components/shared/config/configSections/HelpSection";
import LegalSection from "@app/components/shared/config/configSections/LegalSection";
import {
BackendThirdPartyLicensesSection,
FrontendThirdPartyLicensesSection,
} from "@app/components/shared/config/configSections/ThirdPartyLicensesSection";
import type {
ConfigNavItem,
ConfigNavSection,
} from "@app/components/shared/config/types";
// Keep the modal shell light. Settings sections are independent screens and
// do not need to be evaluated just to build the navigation list.
const HotkeysSection = lazy(
() => import("@app/components/shared/config/configSections/HotkeysSection"),
);
const GeneralSection = lazy(
() => import("@app/components/shared/config/configSections/GeneralSection"),
);
const HelpSection = lazy(
() => import("@app/components/shared/config/configSections/HelpSection"),
);
const LegalSection = lazy(
() => import("@app/components/shared/config/configSections/LegalSection"),
);
const BackendThirdPartyLicensesSection = lazy(() =>
import("@app/components/shared/config/configSections/ThirdPartyLicensesSection").then(
({ BackendThirdPartyLicensesSection: Component }) => ({
default: Component,
}),
),
);
const FrontendThirdPartyLicensesSection = lazy(() =>
import("@app/components/shared/config/configSections/ThirdPartyLicensesSection").then(
({ FrontendThirdPartyLicensesSection: Component }) => ({
default: Component,
}),
),
);
// Re-exported for the many existing importers; the definitions live in
// config/types so type-only consumers don't pull the section tree in.
export type { ConfigNavItem, ConfigNavSection };
+42 -12
View File
@@ -26,7 +26,9 @@ import Workbench from "@app/components/layout/Workbench";
import FileSidebar from "@app/components/shared/FileSidebar";
import FileManager from "@app/components/FileManager";
import LocalIcon from "@app/components/shared/LocalIcon";
import AppConfigModal from "@app/components/shared/AppConfigModalLazy";
import AppConfigModal, {
preloadAppConfigModal,
} from "@app/components/shared/AppConfigModalLazy";
import { getStartupNavigationAction } from "@app/utils/homePageNavigation";
import { EDITOR_BASENAME } from "@app/routes/editorBasename";
import { HomePageExtensions } from "@app/components/home/HomePageExtensions";
@@ -91,7 +93,10 @@ export default function HomePage() {
const sliderRef = useRef<HTMLDivElement | null>(null);
const [activeMobileView, setActiveMobileView] = useState<MobileView>("tools");
const isProgrammaticScroll = useRef(false);
const [configModalOpen, setConfigModalOpen] = useState(false);
const openConfigModal = useCallback(() => {
preloadAppConfigModal();
window.dispatchEvent(new Event("appConfig:open"));
}, []);
const location = useLocation();
// Persisted user preference for the FileSidebar collapsed state. Auto-
// collapse on /files is layered on top in the transition effect below and
@@ -476,7 +481,7 @@ export default function HomePage() {
variant="tertiary"
className="mobile-bottom-button"
aria-label={t("quickAccess.config", "Config")}
onClick={() => setConfigModalOpen(true)}
onClick={openConfigModal}
>
<LocalIcon
icon="settings-rounded"
@@ -489,10 +494,7 @@ export default function HomePage() {
</Button>
</div>
<FileManager selectedTool={selectedTool} />
<AppConfigModal
opened={configModalOpen}
onClose={handleCloseConfig}
/>
<AppConfigModalHost />
</div>
) : (
<Group
@@ -533,16 +535,13 @@ export default function HomePage() {
return next;
});
}}
onOpenSettings={() => setConfigModalOpen(true)}
onOpenSettings={openConfigModal}
/>
<FolderTreePanel active={navigationState.workbench === "myFiles"} />
<Workbench />
{!hideToolPanel && <RightSidebar />}
<FileManager selectedTool={selectedTool} />
<AppConfigModal
opened={configModalOpen}
onClose={handleCloseConfig}
/>
<AppConfigModalHost />
</Group>
)}
</FilesPageProvider>
@@ -550,6 +549,37 @@ export default function HomePage() {
);
}
/**
* Keeps settings visibility state out of HomePage. The editor page is a large
* tree, so toggling the modal must not cause the whole workbench to render.
*/
function AppConfigModalHost() {
const [opened, setOpened] = useState(false);
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
const handleOpen = () => setOpened(true);
window.addEventListener("appConfig:open", handleOpen);
return () => window.removeEventListener("appConfig:open", handleOpen);
}, []);
// Open the config modal whenever the URL is /settings/* (e.g. from the
// admin tour's openConfigModal action which navigates to /settings/overview).
useEffect(() => {
if (location.pathname.startsWith("/settings")) setOpened(true);
}, [location.pathname]);
const handleClose = useCallback(() => {
setOpened(false);
if (location.pathname.startsWith("/settings")) {
navigate("/", { replace: true });
}
}, [location.pathname, navigate]);
return <AppConfigModal opened={opened} onClose={handleClose} />;
}
interface MyFilesAwareFileSidebarProps extends FileSidebarProps {
active: boolean;
}
@@ -1,8 +1,11 @@
import type { ComponentProps } from "react";
import { lazy, type ComponentProps } from "react";
import { Stack } from "@mantine/core";
import GeneralSection from "@app/components/shared/config/configSections/GeneralSection";
import { LoginLandingSetting } from "@app/components/shared/config/LoginLandingSetting";
const GeneralSection = lazy(
() => import("@app/components/shared/config/configSections/GeneralSection"),
);
type GeneralSectionProps = ComponentProps<typeof GeneralSection>;
/**
@@ -1,34 +1,100 @@
import React from "react";
import React, { lazy } from "react";
import { useTranslation } from "react-i18next";
import {
useConfigNavSections as useCoreConfigNavSections,
ConfigNavSection,
} from "@core/components/shared/config/configNavSections";
import PeopleSection from "@app/components/shared/config/configSections/PeopleSection";
import TeamsSection from "@app/components/shared/config/configSections/TeamsSection";
import AdminGeneralSection from "@app/components/shared/config/configSections/AdminGeneralSection";
import AdminSecuritySection from "@app/components/shared/config/configSections/AdminSecuritySection";
import AdminConnectionsSection from "@app/components/shared/config/configSections/AdminConnectionsSection";
import AdminPrivacySection from "@app/components/shared/config/configSections/AdminPrivacySection";
import AdminDatabaseSection from "@app/components/shared/config/configSections/AdminDatabaseSection";
import AdminAdvancedSection from "@app/components/shared/config/configSections/AdminAdvancedSection";
import AdminLegalSection from "@app/components/shared/config/configSections/AdminLegalSection";
import AdminPlanSection from "@app/components/shared/config/configSections/AdminPlanSection";
import AdminFeaturesSection from "@app/components/shared/config/configSections/AdminFeaturesSection";
import AdminEndpointsSection from "@app/components/shared/config/configSections/AdminEndpointsSection";
import AdminMcpSection from "@app/components/shared/config/configSections/AdminMcpSection";
import AdminAiGeneralSection from "@app/components/shared/config/configSections/AdminAiGeneralSection";
import AdminAiModelsSection from "@app/components/shared/config/configSections/AdminAiModelsSection";
import AdminAiDocumentsSection from "@app/components/shared/config/configSections/AdminAiDocumentsSection";
import AdminAiLimitsSection from "@app/components/shared/config/configSections/AdminAiLimitsSection";
import AdminAuditSection from "@app/components/shared/config/configSections/AdminAuditSection";
import AdminUsageSection from "@app/components/shared/config/configSections/AdminUsageSection";
import AdminStorageSharingSection from "@app/components/shared/config/configSections/AdminStorageSharingSection";
import AdminFolderAccessSection from "@app/components/shared/config/configSections/AdminFolderAccessSection";
import ApiKeys from "@app/components/shared/config/configSections/ApiKeys";
import AccountSection from "@app/components/shared/config/configSections/AccountSection";
import GeneralWithLoginLanding from "@app/components/shared/config/GeneralWithLoginLanding";
// These screens are only needed after selecting their navigation item. Keeping
// them out of the initial settings render avoids evaluating the whole admin UI.
const PeopleSection = lazy(
() => import("@app/components/shared/config/configSections/PeopleSection"),
);
const TeamsSection = lazy(
() => import("@app/components/shared/config/configSections/TeamsSection"),
);
const AdminGeneralSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminGeneralSection"),
);
const AdminSecuritySection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminSecuritySection"),
);
const AdminConnectionsSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminConnectionsSection"),
);
const AdminPrivacySection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminPrivacySection"),
);
const AdminDatabaseSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminDatabaseSection"),
);
const AdminAdvancedSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAdvancedSection"),
);
const AdminLegalSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminLegalSection"),
);
const AdminPlanSection = lazy(
() => import("@app/components/shared/config/configSections/AdminPlanSection"),
);
const AdminFeaturesSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminFeaturesSection"),
);
const AdminEndpointsSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminEndpointsSection"),
);
const AdminMcpSection = lazy(
() => import("@app/components/shared/config/configSections/AdminMcpSection"),
);
const AdminAiGeneralSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAiGeneralSection"),
);
const AdminAiModelsSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAiModelsSection"),
);
const AdminAiDocumentsSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAiDocumentsSection"),
);
const AdminAiLimitsSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAiLimitsSection"),
);
const AdminAuditSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminAuditSection"),
);
const AdminUsageSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminUsageSection"),
);
const AdminStorageSharingSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminStorageSharingSection"),
);
const AdminFolderAccessSection = lazy(
() =>
import("@app/components/shared/config/configSections/AdminFolderAccessSection"),
);
const ApiKeys = lazy(
() => import("@app/components/shared/config/configSections/ApiKeys"),
);
const AccountSection = lazy(
() => import("@app/components/shared/config/configSections/AccountSection"),
);
/**
* Hook version of proprietary config nav sections with proper i18n support
*/