Line data Source code
1 : import * as React from 'react';
2 :
3 : import { useAppConfig, useAppReconfigure } from './app_config.js';
4 : import { useStorageReader } from '../storage/storage_provider.js';
5 : import { useLogger } from '../platform/logger/logger_provider.js';
6 : import { useAppFlavorSelect } from './flavor_provider.js';
7 : import { applyColorSchemeToDocument } from './color_scheme.js';
8 :
9 1 : const LOG_CTX = 'app_settings_sync';
10 :
11 : type Props = {
12 : children: React.ReactElement;
13 : };
14 :
15 1 : const APP_SETTINGS_READY_CTX = React.createContext(false);
16 :
17 1 : export const useAppSettingsReady = (): boolean => React.useContext(APP_SETTINGS_READY_CTX);
18 :
19 : /// Hydrates AppConfig.settings from the persisted manifest once StorageProvider
20 : /// is ready, and persists subsequent changes back to the manifest.
21 : /// Selects the flavor of the settings, a flavor requested by the url still wins.
22 1 : export const AppSettingsSync: React.FC<Props> = (props: Props) => {
23 1 : const config = useAppConfig();
24 1 : const reconfigure = useAppReconfigure();
25 1 : const storageReader = useStorageReader();
26 1 : const logger = useLogger();
27 1 : const selectFlavor = useAppFlavorSelect();
28 :
29 1 : const [hydrated, setHydrated] = React.useState(false);
30 1 : const hydrating = React.useRef(false);
31 :
32 1 : React.useEffect(() => {
33 1 : if (hydrating.current) return;
34 1 : if (config == null) return;
35 1 : hydrating.current = true;
36 1 : storageReader.backend.loadAppSettings().then(stored => {
37 1 : if (stored != null) {
38 1 : logger.info("Hydrated app settings from manifest", {}, LOG_CTX);
39 : // Drop retired settings, the AI provider settings may hold credentials.
40 : // The dashboards and the dataflow were experimental views and are always shown now.
41 1 : const {
42 1 : lastOpenedSessionId: _lastOpenedSessionId,
43 1 : aiProvider: _aiProvider,
44 1 : experimental_dashboards: _experimentalDashboards,
45 1 : experimental_dataflow: _experimentalDataflow,
46 1 : ...settings
47 1 : } = stored as typeof stored & {
48 : lastOpenedSessionId?: string;
49 : aiProvider?: unknown;
50 : experimental_dashboards?: boolean;
51 : experimental_dataflow?: boolean;
52 : };
53 1 : reconfigure(c => c == null ? null : {
54 1 : ...c,
55 1 : settings: { ...(c.settings ?? {}), ...settings },
56 1 : });
57 : // Select the flavor in the same render as the hydration, the app setup loads the fonts of the flavor
58 1 : selectFlavor(settings.flavor ?? config.settings?.flavor);
59 1 : }
60 1 : }).catch(e => {
61 1 : logger.warn("Failed to load app settings from manifest", { error: String(e) }, LOG_CTX);
62 1 : }).finally(() => {
63 1 : setHydrated(true);
64 1 : });
65 1 : }, [config, storageReader, reconfigure, selectFlavor, logger]);
66 :
67 : // Select the flavor when the settings change it
68 1 : const flavorId = config?.settings?.flavor;
69 1 : React.useLayoutEffect(() => {
70 1 : selectFlavor(flavorId);
71 1 : }, [flavorId, selectFlavor]);
72 :
73 : // Apply the color scheme of the settings, the document follows the OS until they are hydrated
74 1 : const colorScheme = config?.settings?.colorScheme ?? null;
75 1 : React.useLayoutEffect(() => {
76 1 : applyColorSchemeToDocument(colorScheme);
77 1 : }, [colorScheme]);
78 :
79 1 : React.useEffect(() => {
80 1 : if (!hydrated) return;
81 1 : if (config?.settings == null) return;
82 1 : const settings = config.settings;
83 1 : const handle = setTimeout(() => {
84 1 : storageReader.backend.saveAppSettings(settings).catch(e => {
85 1 : logger.warn("Failed to persist app settings", { error: String(e) }, LOG_CTX);
86 1 : });
87 1 : }, 250);
88 1 : return () => clearTimeout(handle);
89 1 : }, [hydrated, config?.settings, storageReader, logger]);
90 :
91 1 : return <APP_SETTINGS_READY_CTX.Provider value={hydrated}>{props.children}</APP_SETTINGS_READY_CTX.Provider>;
92 1 : };
|