From fff5fee0e5def56d144819bf9b2d33135a921f59 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Tue, 17 Dec 2024 15:43:48 +0000 Subject: [PATCH 01/14] create AppParameters provider to receive CLI parameters --- packages/suite-base/src/App.test.tsx | 24 ++++++- packages/suite-base/src/App.tsx | 70 ++++++++++--------- .../src/context/AppParametersContext.ts | 24 +++++++ .../providers/AppParametersProvider.test.tsx | 45 ++++++++++++ .../src/providers/AppParametersProvider.tsx | 27 +++++++ .../CurrentLayoutProvider/index.test.tsx | 9 +-- .../providers/CurrentLayoutProvider/index.tsx | 17 ++++- packages/suite-desktop/src/common/types.ts | 5 ++ packages/suite-desktop/src/main/index.ts | 11 ++- .../src/main/parseCLIFlags.test.ts | 44 ++++++++++++ .../suite-desktop/src/main/parseCLIFlags.ts | 25 +++++++ packages/suite-desktop/src/preload/index.ts | 3 + packages/suite-desktop/src/renderer/Root.tsx | 47 +++++++------ packages/suite-desktop/src/renderer/index.tsx | 6 ++ 14 files changed, 292 insertions(+), 65 deletions(-) create mode 100644 packages/suite-base/src/context/AppParametersContext.ts create mode 100644 packages/suite-base/src/providers/AppParametersProvider.test.tsx create mode 100644 packages/suite-base/src/providers/AppParametersProvider.tsx create mode 100644 packages/suite-desktop/src/main/parseCLIFlags.test.ts create mode 100644 packages/suite-desktop/src/main/parseCLIFlags.ts diff --git a/packages/suite-base/src/App.test.tsx b/packages/suite-base/src/App.test.tsx index 9dacd9b8e7..ea68b883ac 100644 --- a/packages/suite-base/src/App.test.tsx +++ b/packages/suite-base/src/App.test.tsx @@ -21,6 +21,7 @@ import NativeWindowContext, { INativeWindow, } from "@lichtblick/suite-base/context/NativeWindowContext"; import { UserScriptStateProvider } from "@lichtblick/suite-base/context/UserScriptStateContext"; +import AppParametersProvider from "@lichtblick/suite-base/providers/AppParametersProvider"; import CurrentLayoutProvider from "@lichtblick/suite-base/providers/CurrentLayoutProvider"; import EventsProvider from "@lichtblick/suite-base/providers/EventsProvider"; import ExtensionCatalogProvider from "@lichtblick/suite-base/providers/ExtensionCatalogProvider"; @@ -30,8 +31,9 @@ import ProblemsContextProvider from "@lichtblick/suite-base/providers/ProblemsCo import { StudioLogsSettingsProvider } from "@lichtblick/suite-base/providers/StudioLogsSettingsProvider"; import TimelineInteractionStateProvider from "@lichtblick/suite-base/providers/TimelineInteractionStateProvider"; import UserProfileLocalStorageProvider from "@lichtblick/suite-base/providers/UserProfileLocalStorageProvider"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; -import { App } from "./App"; +import { App, AppProps } from "./App"; import Workspace from "./Workspace"; function mockProvider(testId: string) { @@ -41,6 +43,7 @@ function mockProvider(testId: string) { // Mocking shared providers and components jest.mock("./providers/LayoutManagerProvider", () => mockProvider("layout-manager-provider")); jest.mock("./providers/PanelCatalogProvider", () => mockProvider("panel-catalog-provider")); +jest.mock("./providers/AppParametersProvider", () => mockProvider("app-parameters-provider")); jest.mock("./components/MultiProvider", () => mockProvider("multi-provider")); jest.mock("./components/StudioToastProvider", () => mockProvider("studio-toast-provider")); jest.mock("./components/GlobalCss", () => mockProvider("global-css")); @@ -70,8 +73,9 @@ const mockAppConfiguration: IAppConfiguration = { }; // Helper to render the App with default props -const setup = (overrides: Partial> = {}) => { - const defaultProps = { +const setup = (overrides: Partial = {}) => { + const defaultProps: AppProps = { + appParameters: {}, appConfiguration: mockAppConfiguration, deepLinks: [], dataSources: [], @@ -89,6 +93,7 @@ describe("App Component", () => { it("renders without crashing", () => { setup(); + expect(screen.getByTestId("app-parameters-provider")).toBeDefined(); expect(screen.getByTestId("color-scheme-theme")).toBeDefined(); expect(screen.getByTestId("css-baseline")).toBeDefined(); expect(screen.getByTestId("error-boundary")).toBeDefined(); @@ -188,6 +193,19 @@ describe("App Component MultiProvider Tests", () => { }); }); + it("verifies that AppParametersProvider is called with correct parameters", () => { + const appParameters = { + [BasicBuilder.string()]: BasicBuilder.string(), + [BasicBuilder.string()]: BasicBuilder.string(), + [BasicBuilder.string()]: BasicBuilder.string(), + }; + setup({ appParameters }); + expect(screen.getByTestId("app-parameters-provider")).toBeDefined(); + + const props = (AppParametersProvider as jest.Mock).mock.calls[0][0]; + expect(props.appParameters).toBe(appParameters); + }); + it("verifies that MultiProvider has rendered all providers when its nativeApp", () => { setup({ nativeAppMenu: {} as INativeAppMenu }); expect(extractProviderTypes()).toContain(NativeAppMenuContext.Provider); diff --git a/packages/suite-base/src/App.tsx b/packages/suite-base/src/App.tsx index 2eb8f594bb..b7e664f908 100644 --- a/packages/suite-base/src/App.tsx +++ b/packages/suite-base/src/App.tsx @@ -11,8 +11,10 @@ import { HTML5Backend } from "react-dnd-html5-backend"; import { IdbLayoutStorage } from "@lichtblick/suite-base/IdbLayoutStorage"; import GlobalCss from "@lichtblick/suite-base/components/GlobalCss"; +import { AppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; import LayoutStorageContext from "@lichtblick/suite-base/context/LayoutStorageContext"; import { UserScriptStateProvider } from "@lichtblick/suite-base/context/UserScriptStateContext"; +import AppParametersProvider from "@lichtblick/suite-base/providers/AppParametersProvider"; import EventsProvider from "@lichtblick/suite-base/providers/EventsProvider"; import LayoutManagerProvider from "@lichtblick/suite-base/providers/LayoutManagerProvider"; import ProblemsContextProvider from "@lichtblick/suite-base/providers/ProblemsContextProvider"; @@ -42,10 +44,11 @@ import PanelCatalogProvider from "./providers/PanelCatalogProvider"; import { LaunchPreference } from "./screens/LaunchPreference"; import { ExtensionLoader } from "./services/ExtensionLoader"; -type AppProps = CustomWindowControlsProps & { - deepLinks: string[]; +export type AppProps = CustomWindowControlsProps & { appConfiguration: IAppConfiguration; + appParameters: AppParameters; dataSources: IDataSourceFactory[]; + deepLinks: string[]; extensionLoaders: readonly ExtensionLoader[]; layoutLoaders: readonly LayoutLoader[]; nativeAppMenu?: INativeAppMenu; @@ -70,6 +73,7 @@ function contextMenuHandler(event: MouseEvent) { export function App(props: AppProps): React.JSX.Element { const { appConfiguration, + appParameters = {}, dataSources, extensionLoaders, layoutLoaders, @@ -128,36 +132,38 @@ export function App(props: AppProps): React.JSX.Element { return ( - - {enableGlobalCss && } - - - - - - - - }> - - - - - - - - - - + + + {enableGlobalCss && } + + + + + + + + }> + + + + + + + + + + + ); } diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts new file mode 100644 index 0000000000..b92603bf4d --- /dev/null +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import { createContext, useContext } from "react"; + +import { Immutable } from "@lichtblick/suite"; + +export type AppParameters = Readonly>; + +export type AppParametersContext = Immutable; + +export const AppParametersContext = createContext(undefined); + +export function useAppParameters(): AppParameters { + const context = useContext(AppParametersContext); + if (context == undefined) { + throw new Error("A LayoutManager provider is required to useLayoutManager"); + } + return context; +} diff --git a/packages/suite-base/src/providers/AppParametersProvider.test.tsx b/packages/suite-base/src/providers/AppParametersProvider.test.tsx new file mode 100644 index 0000000000..73a782a141 --- /dev/null +++ b/packages/suite-base/src/providers/AppParametersProvider.test.tsx @@ -0,0 +1,45 @@ +/** @jest-environment jsdom */ + +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { render } from "@testing-library/react"; + +import { useAppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; + +import AppParametersProvider from "./AppParametersProvider"; + +describe("AppParametersProvider", () => { + it("provides app parameters to its children", () => { + const mockParameters = { key: BasicBuilder.string() }; + const TestComponent = () => { + const appParameters = useAppParameters(); + return
{appParameters.key}
; + }; + + const { getByText } = render( + + + , + ); + + expect(getByText(mockParameters.key)).toBeDefined(); + }); + + it("provides default app parameters when none are given", () => { + const TestComponent = () => { + const appParameters = useAppParameters(); + expect(Object.keys(appParameters)).toHaveLength(0); + return
{Object.keys(appParameters).length}
; + }; + + const { getByText } = render( + + + , + ); + + expect(getByText("0")).toBeDefined(); + }); +}); diff --git a/packages/suite-base/src/providers/AppParametersProvider.tsx b/packages/suite-base/src/providers/AppParametersProvider.tsx new file mode 100644 index 0000000000..f5f97f994f --- /dev/null +++ b/packages/suite-base/src/providers/AppParametersProvider.tsx @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/ + +import { PropsWithChildren, useMemo } from "react"; + +import { + AppParameters, + AppParametersContext, +} from "@lichtblick/suite-base/context/AppParametersContext"; + +type Props = PropsWithChildren<{ + appParameters?: AppParameters; +}>; + +export default function AppParametersProvider({ + children, + appParameters = {}, +}: Props): React.JSX.Element { + const parameters = useMemo(() => appParameters, [appParameters]); + return ( + {children} + ); +} diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx index f2b139e51d..8b7580005e 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx @@ -51,10 +51,10 @@ function makeMockLayoutManager() { isBusy: false, isOnline: false, error: undefined, - on: jest.fn(/*noop*/), - off: jest.fn(/*noop*/), - setError: jest.fn(/*noop*/), - setOnline: jest.fn(/*noop*/), + on: jest.fn(), + off: jest.fn(), + setError: jest.fn(), + setOnline: jest.fn(), getLayouts: jest.fn(), getLayout: jest.fn(), saveNewLayout: jest.fn().mockImplementation(mockThrow("saveNewLayout")), @@ -205,6 +205,7 @@ describe("CurrentLayoutProvider", () => { it("keeps identity of action functions when modifying layout", async () => { const mockLayoutManager = makeMockLayoutManager(); + mockLayoutManager.getLayouts.mockImplementation(() => []); mockLayoutManager.getLayout.mockImplementation(async () => { return { id: "TEST_ID", diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx index 32f30b3a2b..b154edd491 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx @@ -17,6 +17,7 @@ import { useShallowMemo } from "@lichtblick/hooks"; import Logger from "@lichtblick/log"; import { VariableValue } from "@lichtblick/suite"; import { useAnalytics } from "@lichtblick/suite-base/context/AnalyticsContext"; +import { useAppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; import CurrentLayoutContext, { ICurrentLayout, LayoutID, @@ -70,6 +71,8 @@ export default function CurrentLayoutProvider({ const analytics = useAnalytics(); const isMounted = useMountedState(); + const appParameters = useAppParameters(); + const [mosaicId] = useState(() => uuidv4()); const layoutStateListeners = useRef(new Set<(_: LayoutState) => void>()); @@ -277,6 +280,17 @@ export default function CurrentLayoutProvider({ // Try to load default layouts, before checking to add the fallback "Default". await loadDefaultLayouts(layoutManager, loaders); + const layouts = await layoutManager.getLayouts(); + + // Check if there's a layout specified by the CLI + const defaultLayoutFromCLI = layouts.find( + (layout) => layout.name === appParameters.defaultLayout, + ); + if (defaultLayoutFromCLI) { + await setSelectedLayoutId(defaultLayoutFromCLI.id); + return; + } + // Retreive the selected layout id from the user's profile. If there's no layout specified // or we can't load it then save and select a default layout. const { currentLayoutId } = await getUserProfile(); @@ -287,7 +301,6 @@ export default function CurrentLayoutProvider({ return; } - const layouts = await layoutManager.getLayouts(); if (layouts.length > 0) { const sortedLayouts = [...layouts].sort((a, b) => a.name.localeCompare(b.name)); await setSelectedLayoutId(sortedLayouts[0]!.id); @@ -302,7 +315,7 @@ export default function CurrentLayoutProvider({ await setSelectedLayoutId(newLayout.id); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getUserProfile, layoutManager, setSelectedLayoutId]); + }, [getUserProfile, layoutManager, setSelectedLayoutId, enqueueSnackbar]); const { updateSharedPanelState } = useUpdateSharedPanelState(layoutStateRef, setLayoutState); diff --git a/packages/suite-desktop/src/common/types.ts b/packages/suite-desktop/src/common/types.ts index 4cd620a9c5..ba7fbfaed6 100644 --- a/packages/suite-desktop/src/common/types.ts +++ b/packages/suite-desktop/src/common/types.ts @@ -75,6 +75,8 @@ type DesktopLayout = { from: string; }; +export type CLIFlags = Readonly>; + interface Desktop { /** https://www.electronjs.org/docs/tutorial/represented-file */ setRepresentedFilename(path: string | undefined): Promise; @@ -109,6 +111,9 @@ interface Desktop { // was not found (i.e. already uninstalled) uninstallExtension: (id: string) => Promise; + // Get CLI flags passed when the app was launched + getCLIFlags: () => Promise; + /** Handle a double-click on the custom title bar */ handleTitleBarDoubleClick(): void; diff --git a/packages/suite-desktop/src/main/index.ts b/packages/suite-desktop/src/main/index.ts index 93b9f65ef4..964dbbdcd2 100644 --- a/packages/suite-desktop/src/main/index.ts +++ b/packages/suite-desktop/src/main/index.ts @@ -18,6 +18,7 @@ import StudioWindow from "./StudioWindow"; import getDevModeIcon from "./getDevModeIcon"; import injectFilesToOpen from "./injectFilesToOpen"; import installChromeExtensions from "./installChromeExtensions"; +import { parseCLIFlags } from "./parseCLIFlags"; import { registerRosPackageProtocolHandlers, registerRosPackageProtocolSchemes, @@ -120,7 +121,10 @@ export async function main(): Promise { app.emit("open-url", { preventDefault() {} }, link); } - const files = argv.slice(1).filter((arg) => isFileToOpen(arg)); + const files = argv + .slice(1) + .filter((arg) => !arg.startsWith("--")) // Filter out flags + .filter((arg) => isFileToOpen(arg)); for (const file of files) { app.emit("open-file", { preventDefault() {} }, file); } @@ -143,6 +147,7 @@ export async function main(): Promise { // or command line arguments. const filesToOpen: string[] = process.argv .slice(1) + .filter((arg) => !arg.startsWith("--")) // Filter out flags .map((filePath) => path.resolve(filePath)) // Convert to absolute path, linux has some problems to resolve relative paths .filter(isFileToOpen); @@ -209,6 +214,10 @@ export async function main(): Promise { ipcMain.handle("getUserDataPath", () => app.getPath("userData")); ipcMain.handle("getHomePath", () => app.getPath("home")); + // Get the command line flags passed to the app when it was launched + const parsedCLIFlags = parseCLIFlags(process.argv); + ipcMain.handle("getCLIFlags", () => parsedCLIFlags); + // Must be called before app.ready event registerRosPackageProtocolSchemes(); diff --git a/packages/suite-desktop/src/main/parseCLIFlags.test.ts b/packages/suite-desktop/src/main/parseCLIFlags.test.ts new file mode 100644 index 0000000000..e52511d936 --- /dev/null +++ b/packages/suite-desktop/src/main/parseCLIFlags.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { parseCLIFlags } from "./parseCLIFlags"; + +describe("parseCLIFlags", () => { + it("should parse single flag correctly", () => { + const argv = ["--flag=value"]; + const result = parseCLIFlags(argv); + expect(result).toEqual({ flag: "value" }); + }); + + it("should parse multiple flags correctly", () => { + const argv = ["--flag1=value1", "--flag2=value2"]; + const result = parseCLIFlags(argv); + expect(result).toEqual({ flag1: "value1", flag2: "value2" }); + }); + + it("should overwrite duplicated flags", () => { + const argv = ["--flag=value1", "--flag=value2"]; + const result = parseCLIFlags(argv); + expect(result).toEqual({ flag: "value2" }); + }); + + it("should ignore arguments that do not start with '--'", () => { + const argv = ["--flag1=value1", "someArg", "someOther=Arg", "--flag2=value2"]; + const result = parseCLIFlags(argv); + expect(result).toEqual({ flag1: "value1", flag2: "value2" }); + }); + + it("should return an empty object if no valid flags are provided", () => { + const argv = ["someArg", "--flag", "--flag1=", "--=value1"]; + const result = parseCLIFlags(argv); + expect(result).toEqual({}); + }); + + it("should return a readonly object", () => { + const argv = ["--flag1=value1"]; + const result = parseCLIFlags(argv); + expect(() => { + (result as any).flag1 = "newValue"; + }).toThrow(); + }); +}); diff --git a/packages/suite-desktop/src/main/parseCLIFlags.ts b/packages/suite-desktop/src/main/parseCLIFlags.ts new file mode 100644 index 0000000000..d0c034393c --- /dev/null +++ b/packages/suite-desktop/src/main/parseCLIFlags.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +import { CLIFlags } from "../common/types"; + +/** + * Parses CLI flags from the process arguments and returns a readonly record. + * Flags are expected to start with '--'. + * + * @param argv - The process arguments to parse (e.g., process.argv). + * @returns A readonly record containing the flag names as keys and their corresponding values as strings. + */ +export function parseCLIFlags(argv: string[]): CLIFlags { + const flags = argv.reduce>((prev, curr) => { + if (curr.startsWith("--")) { + const [key, value] = curr.slice(2).split("="); + if (key && value) { + prev[key] = value; + } + } + return prev; + }, {}); + + return Object.freeze(flags); +} diff --git a/packages/suite-desktop/src/preload/index.ts b/packages/suite-desktop/src/preload/index.ts index c2e8541eea..06b7d4ebe6 100644 --- a/packages/suite-desktop/src/preload/index.ts +++ b/packages/suite-desktop/src/preload/index.ts @@ -117,6 +117,9 @@ export function main(): void { async updateLanguage() { await ipcRenderer.invoke("updateLanguage"); }, + async getCLIFlags(): Promise>> { + return await (ipcRenderer.invoke("getCLIFlags") as Promise>>); + }, getDeepLinks(): string[] { return deepLinks; }, diff --git a/packages/suite-desktop/src/renderer/Root.tsx b/packages/suite-desktop/src/renderer/Root.tsx index 8747a61d72..f3a1dbf0ae 100644 --- a/packages/suite-desktop/src/renderer/Root.tsx +++ b/packages/suite-desktop/src/renderer/Root.tsx @@ -25,6 +25,7 @@ import { UlogLocalDataSourceFactory, VelodyneDataSourceFactory, } from "@lichtblick/suite-base"; +import { AppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; import { DesktopExtensionLoader } from "./services/DesktopExtensionLoader"; import { DesktopLayoutLoader } from "./services/DesktopLayoutLoader"; @@ -38,6 +39,7 @@ const menuBridge = (global as { menuBridge?: NativeMenuBridge }).menuBridge; const ctxbridge = (global as { ctxbridge?: OsContext }).ctxbridge; export default function Root(props: { + appParameters: AppParameters; appConfiguration: IAppConfiguration; extraProviders: React.JSX.Element[] | undefined; dataSources: IDataSourceFactory[] | undefined; @@ -148,28 +150,27 @@ export default function Root(props: { }, []); return ( - <> - { - nativeWindow.handleTitleBarDoubleClick(); - }} - showCustomWindowControls={ctxbridge?.platform === "linux"} - isMaximized={isMaximized} - onMinimizeWindow={onMinimizeWindow} - onMaximizeWindow={onMaximizeWindow} - onUnmaximizeWindow={onUnmaximizeWindow} - onCloseWindow={onCloseWindow} - extraProviders={props.extraProviders} - /> - + { + nativeWindow.handleTitleBarDoubleClick(); + }} + showCustomWindowControls={ctxbridge?.platform === "linux"} + isMaximized={isMaximized} + onMinimizeWindow={onMinimizeWindow} + onMaximizeWindow={onMaximizeWindow} + onUnmaximizeWindow={onUnmaximizeWindow} + onCloseWindow={onCloseWindow} + extraProviders={props.extraProviders} + /> ); } diff --git a/packages/suite-desktop/src/renderer/index.tsx b/packages/suite-desktop/src/renderer/index.tsx index 9ca2ffe660..90844c7e60 100644 --- a/packages/suite-desktop/src/renderer/index.tsx +++ b/packages/suite-desktop/src/renderer/index.tsx @@ -23,6 +23,9 @@ import { } from "@lichtblick/suite-base"; import Root from "./Root"; +import { Desktop } from "../common/types"; + +const desktopBridge = (global as unknown as { desktopBridge: Desktop }).desktopBridge; const log = Logger.getLogger(__filename); @@ -61,10 +64,13 @@ export async function main(params: MainParams): Promise { await waitForFonts(); await initI18n(); + const cliFlags = await desktopBridge.getCLIFlags(); + const root = createRoot(rootEl); root.render( Date: Tue, 17 Dec 2024 15:49:34 +0000 Subject: [PATCH 02/14] update correct message --- .../src/context/AppParametersContext.ts | 2 +- .../CurrentLayoutProvider/index.test.tsx | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts index b92603bf4d..fdd8404c29 100644 --- a/packages/suite-base/src/context/AppParametersContext.ts +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -18,7 +18,7 @@ export const AppParametersContext = createContext - - - - {children} - - - - - + + + + + + {children} + + + + + + ); }, }, From bb731d908d4ade1ea5cc95c2388977cb9c2e01b6 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 08:53:25 +0000 Subject: [PATCH 03/14] testing --- packages/suite-base/src/AppParameters.ts | 7 +++ .../CurrentLayoutProvider/index.test.tsx | 43 ++++++++++++++++++- .../providers/CurrentLayoutProvider/index.tsx | 18 ++++---- 3 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 packages/suite-base/src/AppParameters.ts diff --git a/packages/suite-base/src/AppParameters.ts b/packages/suite-base/src/AppParameters.ts new file mode 100644 index 0000000000..c8dd1acabf --- /dev/null +++ b/packages/suite-base/src/AppParameters.ts @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +// SPDX-License-Identifier: MPL-2.0 + +export enum AppParameters { + // Used to start with a specific layout passed as parameter + DEFAULT_LAYOUT = "defaultLayout", +} diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx index 9800f97c0b..f4eeac5043 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx @@ -76,9 +76,11 @@ function makeMockUserProfile() { function renderTest({ mockLayoutManager, mockUserProfile, + mockAppParameters = {}, }: { mockLayoutManager: ILayoutManager; mockUserProfile: UserProfileStorage; + mockAppParameters?: Record; }) { const childMounted = new Condvar(); const childMountedWait = childMounted.wait(); @@ -103,7 +105,7 @@ function renderTest({ childMounted.notifyAll(); }, []); return ( - + @@ -281,4 +283,43 @@ describe("CurrentLayoutProvider", () => { expect(selectedLayout).toBeDefined(); expect(selectedLayout).toBe("layout2"); }); + + it("should select a layout though app parameters", async () => { + const mockAppParameters = { defaultLayout: "LAYOUT 2" }; + const mockLayoutManager = makeMockLayoutManager(); + mockLayoutManager.getLayout.mockImplementation(async () => undefined); + mockLayoutManager.getLayouts.mockImplementation(async () => { + return [ + { + id: "layout1", + name: "LAYOUT 1", + data: { data: TEST_LAYOUT }, + }, + { + id: "layout2", + name: "LAYOUT 2", + data: { data: TEST_LAYOUT }, + }, + ]; + }); + + const mockUserProfile = makeMockUserProfile(); + mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: undefined }); + + const { result, all } = renderTest({ + mockLayoutManager, + mockUserProfile, + mockAppParameters, + }); + + await act(async () => { + await result.current.childMounted; + }); + + const selectedLayout = all.find((item) => item.layoutState.selectedLayout?.id)?.layoutState + .selectedLayout?.id; + + expect(selectedLayout).toBeDefined(); + expect(selectedLayout).toBe("layout2"); + }); }); diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx index b154edd491..2b92e5b3bf 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx @@ -16,6 +16,7 @@ import { v4 as uuidv4 } from "uuid"; import { useShallowMemo } from "@lichtblick/hooks"; import Logger from "@lichtblick/log"; import { VariableValue } from "@lichtblick/suite"; +import { AppParameters } from "@lichtblick/suite-base/AppParameters"; import { useAnalytics } from "@lichtblick/suite-base/context/AnalyticsContext"; import { useAppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; import CurrentLayoutContext, { @@ -280,20 +281,21 @@ export default function CurrentLayoutProvider({ // Try to load default layouts, before checking to add the fallback "Default". await loadDefaultLayouts(layoutManager, loaders); + // For some reason, this needs to go befre the setSelectedLayoutId, zprobably some initialization + const { currentLayoutId } = await getUserProfile(); + const layouts = await layoutManager.getLayouts(); - // Check if there's a layout specified by the CLI - const defaultLayoutFromCLI = layouts.find( - (layout) => layout.name === appParameters.defaultLayout, - ); - if (defaultLayoutFromCLI) { - await setSelectedLayoutId(defaultLayoutFromCLI.id); + // Check if there's a layout specified by app parameter + const predefinedLayout = appParameters[AppParameters.DEFAULT_LAYOUT]; + const defaultLayoutFromParameters = layouts.find((l) => l.name === predefinedLayout); + if (defaultLayoutFromParameters) { + await setSelectedLayoutId(defaultLayoutFromParameters.id, { saveToProfile: false }); return; } // Retreive the selected layout id from the user's profile. If there's no layout specified - // or we can't load it then save and select a default layout. - const { currentLayoutId } = await getUserProfile(); + // or we can't load it then save and select a default layout const layout = currentLayoutId ? await layoutManager.getLayout(currentLayoutId) : undefined; if (layout) { From b535fae2e7dced797dbfda9135526060e9585195 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:12:48 +0000 Subject: [PATCH 04/14] clean tests --- packages/suite-base/src/AppParameters.ts | 2 +- .../src/context/AppParametersContext.ts | 3 +- .../CurrentLayoutProvider/index.test.tsx | 28 ++++++++----------- .../providers/CurrentLayoutProvider/index.tsx | 12 ++++---- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/suite-base/src/AppParameters.ts b/packages/suite-base/src/AppParameters.ts index c8dd1acabf..b7c2da6d41 100644 --- a/packages/suite-base/src/AppParameters.ts +++ b/packages/suite-base/src/AppParameters.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) // SPDX-License-Identifier: MPL-2.0 -export enum AppParameters { +export enum AppParametersEnum { // Used to start with a specific layout passed as parameter DEFAULT_LAYOUT = "defaultLayout", } diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts index fdd8404c29..65c57d2478 100644 --- a/packages/suite-base/src/context/AppParametersContext.ts +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -8,8 +8,9 @@ import { createContext, useContext } from "react"; import { Immutable } from "@lichtblick/suite"; +import { AppParametersEnum } from "@lichtblick/suite-base/AppParameters"; -export type AppParameters = Readonly>; +export type AppParameters = Readonly>; export type AppParametersContext = Immutable; diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx index f4eeac5043..893b2e08c0 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx @@ -125,6 +125,17 @@ function renderTest({ } describe("CurrentLayoutProvider", () => { + const mockLayoutManager = makeMockLayoutManager(); + const mockUserProfile = makeMockUserProfile(); + + beforeEach(() => { + jest.clearAllMocks(); + // Default mocks + mockLayoutManager.getLayout.mockImplementation(async () => undefined); + mockLayoutManager.getLayouts.mockImplementation(() => []); + mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: undefined }); + }); + afterEach(() => { (console.warn as jest.Mock).mockClear(); }); @@ -139,7 +150,6 @@ describe("CurrentLayoutProvider", () => { }; const condvar = new Condvar(); const layoutStorageGetCalledWait = condvar.wait(); - const mockLayoutManager = makeMockLayoutManager(); mockLayoutManager.getLayout.mockImplementation(async () => { condvar.notifyAll(); return { @@ -149,7 +159,6 @@ describe("CurrentLayoutProvider", () => { }; }); - const mockUserProfile = makeMockUserProfile(); mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: "example" }); const { all } = renderTest({ mockLayoutManager, mockUserProfile }); @@ -183,7 +192,6 @@ describe("CurrentLayoutProvider", () => { const condvar = new Condvar(); const layoutStorageGetCalledWait = condvar.wait(); - const mockLayoutManager = makeMockLayoutManager(); mockLayoutManager.getLayout.mockImplementation(async () => { condvar.notifyAll(); return { @@ -193,7 +201,6 @@ describe("CurrentLayoutProvider", () => { }; }); - const mockUserProfile = makeMockUserProfile(); mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: "example" }); const { all } = renderTest({ mockLayoutManager, mockUserProfile }); @@ -209,8 +216,6 @@ describe("CurrentLayoutProvider", () => { }); it("keeps identity of action functions when modifying layout", async () => { - const mockLayoutManager = makeMockLayoutManager(); - mockLayoutManager.getLayouts.mockImplementation(() => []); mockLayoutManager.getLayout.mockImplementation(async () => { return { id: "TEST_ID", @@ -226,7 +231,6 @@ describe("CurrentLayoutProvider", () => { baseline: { data: TEST_LAYOUT, updatedAt: new Date(10).toISOString() }, }; }); - const mockUserProfile = makeMockUserProfile(); mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: "example" }); const { result } = renderTest({ @@ -248,8 +252,6 @@ describe("CurrentLayoutProvider", () => { }); it("selects the first layout in alphabetic order, when there is no selected layout", async () => { - const mockLayoutManager = makeMockLayoutManager(); - mockLayoutManager.getLayout.mockImplementation(async () => undefined); mockLayoutManager.getLayouts.mockImplementation(async () => { return [ { @@ -265,9 +267,6 @@ describe("CurrentLayoutProvider", () => { ]; }); - const mockUserProfile = makeMockUserProfile(); - mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: undefined }); - const { result, all } = renderTest({ mockLayoutManager, mockUserProfile, @@ -286,8 +285,6 @@ describe("CurrentLayoutProvider", () => { it("should select a layout though app parameters", async () => { const mockAppParameters = { defaultLayout: "LAYOUT 2" }; - const mockLayoutManager = makeMockLayoutManager(); - mockLayoutManager.getLayout.mockImplementation(async () => undefined); mockLayoutManager.getLayouts.mockImplementation(async () => { return [ { @@ -303,9 +300,6 @@ describe("CurrentLayoutProvider", () => { ]; }); - const mockUserProfile = makeMockUserProfile(); - mockUserProfile.getUserProfile.mockResolvedValue({ currentLayoutId: undefined }); - const { result, all } = renderTest({ mockLayoutManager, mockUserProfile, diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx index 2b92e5b3bf..911e701a43 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx @@ -16,7 +16,6 @@ import { v4 as uuidv4 } from "uuid"; import { useShallowMemo } from "@lichtblick/hooks"; import Logger from "@lichtblick/log"; import { VariableValue } from "@lichtblick/suite"; -import { AppParameters } from "@lichtblick/suite-base/AppParameters"; import { useAnalytics } from "@lichtblick/suite-base/context/AnalyticsContext"; import { useAppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; import CurrentLayoutContext, { @@ -278,17 +277,16 @@ export default function CurrentLayoutProvider({ return; } + // For some reason, this needs to go befre the setSelectedLayoutId, probably some initialization + const { currentLayoutId } = await getUserProfile(); + // Try to load default layouts, before checking to add the fallback "Default". await loadDefaultLayouts(layoutManager, loaders); - // For some reason, this needs to go befre the setSelectedLayoutId, zprobably some initialization - const { currentLayoutId } = await getUserProfile(); - const layouts = await layoutManager.getLayouts(); - // Check if there's a layout specified by app parameter - const predefinedLayout = appParameters[AppParameters.DEFAULT_LAYOUT]; - const defaultLayoutFromParameters = layouts.find((l) => l.name === predefinedLayout); + // // Check if there's a layout specified by app parameter + const defaultLayoutFromParameters = layouts.find((l) => l.name === appParameters.defaultLayout); if (defaultLayoutFromParameters) { await setSelectedLayoutId(defaultLayoutFromParameters.id, { saveToProfile: false }); return; From 4f08e14e4e51dabe531fa1f08718aa7e97e1a5b1 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:24:10 +0000 Subject: [PATCH 05/14] fix types to become easier to developers --- .../suite-base/src/context/AppParametersContext.ts | 6 +++++- .../src/providers/AppParametersProvider.tsx | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts index 65c57d2478..4433b5b1a7 100644 --- a/packages/suite-base/src/context/AppParametersContext.ts +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -10,7 +10,11 @@ import { createContext, useContext } from "react"; import { Immutable } from "@lichtblick/suite"; import { AppParametersEnum } from "@lichtblick/suite-base/AppParameters"; -export type AppParameters = Readonly>; +// Defines a type for input parameters, allowing any string keys with string values. +export type AppParametersInput = Readonly>; + +// Defines a type for application parameters, restricting keys to the AppParametersEnum for type-safe usage. +export type AppParameters = Readonly>; export type AppParametersContext = Immutable; diff --git a/packages/suite-base/src/providers/AppParametersProvider.tsx b/packages/suite-base/src/providers/AppParametersProvider.tsx index f5f97f994f..d76335b526 100644 --- a/packages/suite-base/src/providers/AppParametersProvider.tsx +++ b/packages/suite-base/src/providers/AppParametersProvider.tsx @@ -10,17 +10,27 @@ import { PropsWithChildren, useMemo } from "react"; import { AppParameters, AppParametersContext, + AppParametersInput, } from "@lichtblick/suite-base/context/AppParametersContext"; type Props = PropsWithChildren<{ - appParameters?: AppParameters; + appParameters?: AppParametersInput; }>; +/** + * AppParametersProvider: + * + * This component provides a context for application parameters within the Lichtblick ecosystem. + * + * Type Safety: + * The `appParameters` input is cast to the `AppParameters` type, ensuring that keys align with + * the expected enumeration. This guarantees proper autocomplete and type-checking for developers. + */ export default function AppParametersProvider({ children, appParameters = {}, }: Props): React.JSX.Element { - const parameters = useMemo(() => appParameters, [appParameters]); + const parameters: AppParameters = useMemo(() => appParameters, [appParameters]); return ( {children} ); From b4745096db78923799c44aae954e47f16f827f96 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:27:07 +0000 Subject: [PATCH 06/14] permit possible undefined --- packages/suite-base/src/context/AppParametersContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts index 4433b5b1a7..7b9d4c3ab7 100644 --- a/packages/suite-base/src/context/AppParametersContext.ts +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -14,7 +14,7 @@ import { AppParametersEnum } from "@lichtblick/suite-base/AppParameters"; export type AppParametersInput = Readonly>; // Defines a type for application parameters, restricting keys to the AppParametersEnum for type-safe usage. -export type AppParameters = Readonly>; +export type AppParameters = Readonly>; export type AppParametersContext = Immutable; From d7e14aae45b2ef0c4155f8f1954f9068295703f5 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:34:20 +0000 Subject: [PATCH 07/14] improve tests --- .../src/providers/CurrentLayoutProvider/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx index 893b2e08c0..b76055ad67 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx @@ -129,7 +129,6 @@ describe("CurrentLayoutProvider", () => { const mockUserProfile = makeMockUserProfile(); beforeEach(() => { - jest.clearAllMocks(); // Default mocks mockLayoutManager.getLayout.mockImplementation(async () => undefined); mockLayoutManager.getLayouts.mockImplementation(() => []); @@ -138,6 +137,7 @@ describe("CurrentLayoutProvider", () => { afterEach(() => { (console.warn as jest.Mock).mockClear(); + jest.clearAllMocks(); }); it("uses currentLayoutId from UserProfile to load from LayoutStorage", async () => { From 224f688c7d4670831042b0524ea3f6c1d9bf555f Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:36:22 +0000 Subject: [PATCH 08/14] export from base --- packages/suite-base/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/suite-base/src/index.ts b/packages/suite-base/src/index.ts index 2c005232de..c19c3d4e7b 100644 --- a/packages/suite-base/src/index.ts +++ b/packages/suite-base/src/index.ts @@ -37,6 +37,7 @@ export type { LayoutInfo } from "./types/layouts"; export type { LayoutData } from "./context/CurrentLayoutContext"; export type { ExtensionInfo, ExtensionNamespace } from "./types/Extensions"; export { AppSetting } from "./AppSetting"; +export { AppParametersEnum } from "./AppParameters"; export { default as FoxgloveWebSocketDataSourceFactory } from "./dataSources/FoxgloveWebSocketDataSourceFactory"; export { default as Ros1LocalBagDataSourceFactory } from "./dataSources/Ros1LocalBagDataSourceFactory"; export { default as Ros1SocketDataSourceFactory } from "./dataSources/Ros1SocketDataSourceFactory"; From 1be776cd7634f1a71b9baf6d5d5812ce254d775f Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:41:15 +0000 Subject: [PATCH 09/14] add context to version web --- packages/suite-base/src/SharedRoot.tsx | 49 ++++++++++++++------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/suite-base/src/SharedRoot.tsx b/packages/suite-base/src/SharedRoot.tsx index d0611c6f96..e693324f67 100644 --- a/packages/suite-base/src/SharedRoot.tsx +++ b/packages/suite-base/src/SharedRoot.tsx @@ -10,6 +10,7 @@ import { ISharedRootContext, SharedRootContext, } from "@lichtblick/suite-base/context/SharedRootContext"; +import AppParametersProvider from "@lichtblick/suite-base/providers/AppParametersProvider"; import { ColorSchemeThemeProvider } from "./components/ColorSchemeThemeProvider"; import CssBaseline from "./components/CssBaseline"; @@ -36,29 +37,31 @@ export function SharedRoot( return ( - - {enableGlobalCss && } - - - - {children} - - - - + + + {enableGlobalCss && } + + + + {children} + + + + + ); } From 38f81a170c4cca46e3223b246d53a10f0f159bbe Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 10:57:27 +0000 Subject: [PATCH 10/14] add more tests --- .../src/context/AppParametersContext.ts | 2 +- .../providers/AppParametersProvider.test.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/suite-base/src/context/AppParametersContext.ts b/packages/suite-base/src/context/AppParametersContext.ts index 7b9d4c3ab7..21b5839a82 100644 --- a/packages/suite-base/src/context/AppParametersContext.ts +++ b/packages/suite-base/src/context/AppParametersContext.ts @@ -23,7 +23,7 @@ export const AppParametersContext = createContext { it("provides app parameters to its children", () => { - const mockParameters = { key: BasicBuilder.string() }; + const mockParameters = { defaultLayout: BasicBuilder.string() }; const TestComponent = () => { const appParameters = useAppParameters(); - return
{appParameters.key}
; + return
{appParameters.defaultLayout}
; }; const { getByText } = render( @@ -24,7 +24,7 @@ describe("AppParametersProvider", () => {
, ); - expect(getByText(mockParameters.key)).toBeDefined(); + expect(getByText(mockParameters.defaultLayout)).toBeDefined(); }); it("provides default app parameters when none are given", () => { @@ -42,4 +42,16 @@ describe("AppParametersProvider", () => { expect(getByText("0")).toBeDefined(); }); + + it("should throw an error if useAppParameters is called without AppParametersProvider", () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + + const TestComponent = () => { + useAppParameters(); + return
; + }; + + expect(() => render()).toThrow(); + consoleErrorSpy.mockRestore(); + }); }); From 1825b13b3b3bd91ce15f9aee501a974a07d60909 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 11:09:19 +0000 Subject: [PATCH 11/14] fix types --- packages/suite-base/src/App.tsx | 4 ++-- packages/suite-base/src/index.ts | 1 + packages/suite-desktop/src/renderer/Root.tsx | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/suite-base/src/App.tsx b/packages/suite-base/src/App.tsx index b7e664f908..4d7bb78fa9 100644 --- a/packages/suite-base/src/App.tsx +++ b/packages/suite-base/src/App.tsx @@ -11,7 +11,7 @@ import { HTML5Backend } from "react-dnd-html5-backend"; import { IdbLayoutStorage } from "@lichtblick/suite-base/IdbLayoutStorage"; import GlobalCss from "@lichtblick/suite-base/components/GlobalCss"; -import { AppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; +import { AppParametersInput } from "@lichtblick/suite-base/context/AppParametersContext"; import LayoutStorageContext from "@lichtblick/suite-base/context/LayoutStorageContext"; import { UserScriptStateProvider } from "@lichtblick/suite-base/context/UserScriptStateContext"; import AppParametersProvider from "@lichtblick/suite-base/providers/AppParametersProvider"; @@ -46,7 +46,7 @@ import { ExtensionLoader } from "./services/ExtensionLoader"; export type AppProps = CustomWindowControlsProps & { appConfiguration: IAppConfiguration; - appParameters: AppParameters; + appParameters: AppParametersInput; dataSources: IDataSourceFactory[]; deepLinks: string[]; extensionLoaders: readonly ExtensionLoader[]; diff --git a/packages/suite-base/src/index.ts b/packages/suite-base/src/index.ts index c19c3d4e7b..072748cc32 100644 --- a/packages/suite-base/src/index.ts +++ b/packages/suite-base/src/index.ts @@ -38,6 +38,7 @@ export type { LayoutData } from "./context/CurrentLayoutContext"; export type { ExtensionInfo, ExtensionNamespace } from "./types/Extensions"; export { AppSetting } from "./AppSetting"; export { AppParametersEnum } from "./AppParameters"; +export type { AppParameters, AppParametersInput } from "./context/AppParametersContext"; export { default as FoxgloveWebSocketDataSourceFactory } from "./dataSources/FoxgloveWebSocketDataSourceFactory"; export { default as Ros1LocalBagDataSourceFactory } from "./dataSources/Ros1LocalBagDataSourceFactory"; export { default as Ros1SocketDataSourceFactory } from "./dataSources/Ros1SocketDataSourceFactory"; diff --git a/packages/suite-desktop/src/renderer/Root.tsx b/packages/suite-desktop/src/renderer/Root.tsx index f3a1dbf0ae..61fe9c4694 100644 --- a/packages/suite-desktop/src/renderer/Root.tsx +++ b/packages/suite-desktop/src/renderer/Root.tsx @@ -25,7 +25,7 @@ import { UlogLocalDataSourceFactory, VelodyneDataSourceFactory, } from "@lichtblick/suite-base"; -import { AppParameters } from "@lichtblick/suite-base/context/AppParametersContext"; +import { AppParametersInput } from "@lichtblick/suite-base/context/AppParametersContext"; import { DesktopExtensionLoader } from "./services/DesktopExtensionLoader"; import { DesktopLayoutLoader } from "./services/DesktopLayoutLoader"; @@ -39,7 +39,7 @@ const menuBridge = (global as { menuBridge?: NativeMenuBridge }).menuBridge; const ctxbridge = (global as { ctxbridge?: OsContext }).ctxbridge; export default function Root(props: { - appParameters: AppParameters; + appParameters: AppParametersInput; appConfiguration: IAppConfiguration; extraProviders: React.JSX.Element[] | undefined; dataSources: IDataSourceFactory[] | undefined; From 3b19772c1f19111727761a3894e1382231b07e53 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 11:45:45 +0000 Subject: [PATCH 12/14] remove import between packages --- packages/suite-base/src/index.ts | 2 -- packages/suite-desktop/src/renderer/Root.tsx | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/suite-base/src/index.ts b/packages/suite-base/src/index.ts index 072748cc32..2c005232de 100644 --- a/packages/suite-base/src/index.ts +++ b/packages/suite-base/src/index.ts @@ -37,8 +37,6 @@ export type { LayoutInfo } from "./types/layouts"; export type { LayoutData } from "./context/CurrentLayoutContext"; export type { ExtensionInfo, ExtensionNamespace } from "./types/Extensions"; export { AppSetting } from "./AppSetting"; -export { AppParametersEnum } from "./AppParameters"; -export type { AppParameters, AppParametersInput } from "./context/AppParametersContext"; export { default as FoxgloveWebSocketDataSourceFactory } from "./dataSources/FoxgloveWebSocketDataSourceFactory"; export { default as Ros1LocalBagDataSourceFactory } from "./dataSources/Ros1LocalBagDataSourceFactory"; export { default as Ros1SocketDataSourceFactory } from "./dataSources/Ros1SocketDataSourceFactory"; diff --git a/packages/suite-desktop/src/renderer/Root.tsx b/packages/suite-desktop/src/renderer/Root.tsx index 61fe9c4694..636435ce50 100644 --- a/packages/suite-desktop/src/renderer/Root.tsx +++ b/packages/suite-desktop/src/renderer/Root.tsx @@ -25,13 +25,12 @@ import { UlogLocalDataSourceFactory, VelodyneDataSourceFactory, } from "@lichtblick/suite-base"; -import { AppParametersInput } from "@lichtblick/suite-base/context/AppParametersContext"; import { DesktopExtensionLoader } from "./services/DesktopExtensionLoader"; import { DesktopLayoutLoader } from "./services/DesktopLayoutLoader"; import { NativeAppMenu } from "./services/NativeAppMenu"; import { NativeWindow } from "./services/NativeWindow"; -import { Desktop, NativeMenuBridge, Storage } from "../common/types"; +import { CLIFlags, Desktop, NativeMenuBridge, Storage } from "../common/types"; const desktopBridge = (global as unknown as { desktopBridge: Desktop }).desktopBridge; const storageBridge = (global as unknown as { storageBridge?: Storage }).storageBridge; @@ -39,7 +38,7 @@ const menuBridge = (global as { menuBridge?: NativeMenuBridge }).menuBridge; const ctxbridge = (global as { ctxbridge?: OsContext }).ctxbridge; export default function Root(props: { - appParameters: AppParametersInput; + appParameters: CLIFlags; appConfiguration: IAppConfiguration; extraProviders: React.JSX.Element[] | undefined; dataSources: IDataSourceFactory[] | undefined; From 9dc671cf14fc44f6dbfa402cbc0dbc9febd027b0 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Wed, 18 Dec 2024 15:19:13 +0000 Subject: [PATCH 13/14] add warning when layout defined is not in the list --- packages/suite-base/src/App.tsx | 8 ++--- packages/suite-base/src/i18n/en/general.ts | 2 ++ .../CurrentLayoutProvider/index.test.tsx | 32 ++++++++++++++++++- .../providers/CurrentLayoutProvider/index.tsx | 12 ++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/suite-base/src/App.tsx b/packages/suite-base/src/App.tsx index 4d7bb78fa9..c8b88d1502 100644 --- a/packages/suite-base/src/App.tsx +++ b/packages/suite-base/src/App.tsx @@ -108,10 +108,6 @@ export function App(props: AppProps): React.JSX.Element { providers.unshift(...extraProviders); } - // The toast and logs provider comes first so they are available to all downstream providers - providers.unshift(); - providers.unshift(); - // Problems provider also must come before other, dependent contexts. providers.unshift(); providers.unshift(); @@ -121,6 +117,10 @@ export function App(props: AppProps): React.JSX.Element { const layoutStorage = useMemo(() => new IdbLayoutStorage(), []); providers.unshift(); + // The toast and logs provider comes first so they are available to all downstream providers + providers.unshift(); + providers.unshift(); + const MaybeLaunchPreference = enableLaunchPreferenceScreen === true ? LaunchPreference : Fragment; useEffect(() => { diff --git a/packages/suite-base/src/i18n/en/general.ts b/packages/suite-base/src/i18n/en/general.ts index 760a3e7b9a..105d78cf06 100644 --- a/packages/suite-base/src/i18n/en/general.ts +++ b/packages/suite-base/src/i18n/en/general.ts @@ -9,4 +9,6 @@ export const general = { foxglove: "Foxglove", learnMore: "Learn more", + noDefaultLayoutParameter: + "The layout '{{layoutName}}' specified in the app parameters does not exist.", }; diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx index b76055ad67..290785d4e0 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.test.tsx @@ -2,12 +2,13 @@ // SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) // SPDX-License-Identifier: MPL-2.0 + // This Source Code Form is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/ import { act, renderHook } from "@testing-library/react"; -import { SnackbarProvider } from "notistack"; +import { SnackbarProvider, useSnackbar } from "notistack"; import { useEffect } from "react"; import { Condvar } from "@lichtblick/den/async"; @@ -28,6 +29,14 @@ import AppParametersProvider from "@lichtblick/suite-base/providers/AppParameter import CurrentLayoutProvider from "@lichtblick/suite-base/providers/CurrentLayoutProvider"; import { MAX_SUPPORTED_LAYOUT_VERSION } from "@lichtblick/suite-base/providers/CurrentLayoutProvider/constants"; import { ILayoutManager } from "@lichtblick/suite-base/services/ILayoutManager"; +import BasicBuilder from "@lichtblick/suite-base/testing/builders/BasicBuilder"; + +jest.mock("notistack", () => ({ + ...jest.requireActual("notistack"), + useSnackbar: jest.fn().mockReturnValue({ + enqueueSnackbar: jest.fn(), + }), +})); const TEST_LAYOUT: LayoutData = { layout: "ExamplePanel!1", @@ -316,4 +325,25 @@ describe("CurrentLayoutProvider", () => { expect(selectedLayout).toBeDefined(); expect(selectedLayout).toBe("layout2"); }); + + it("should show a message to the user if the defaultLayout from app parameter is not found", async () => { + const mockAppParameters = { defaultLayout: BasicBuilder.string() }; + + const { result } = renderTest({ + mockLayoutManager, + mockUserProfile, + mockAppParameters, + }); + + await act(async () => { + await result.current.childMounted; + }); + + const { enqueueSnackbar } = useSnackbar(); + + expect(enqueueSnackbar).toHaveBeenCalledWith( + `The layout '${mockAppParameters.defaultLayout}' specified in the app parameters does not exist.`, + { variant: "warning" }, + ); + }); }); diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx index 911e701a43..93e2f3456d 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx @@ -8,6 +8,7 @@ import * as _ from "lodash-es"; import { useSnackbar } from "notistack"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getNodeAtPath } from "react-mosaic-component"; import { useAsync, useAsyncFn, useMountedState } from "react-use"; import shallowequal from "shallowequal"; @@ -71,6 +72,8 @@ export default function CurrentLayoutProvider({ const analytics = useAnalytics(); const isMounted = useMountedState(); + const { t } = useTranslation("general"); + const appParameters = useAppParameters(); const [mosaicId] = useState(() => uuidv4()); @@ -285,13 +288,20 @@ export default function CurrentLayoutProvider({ const layouts = await layoutManager.getLayouts(); - // // Check if there's a layout specified by app parameter + // Check if there's a layout specified by app parameter const defaultLayoutFromParameters = layouts.find((l) => l.name === appParameters.defaultLayout); if (defaultLayoutFromParameters) { await setSelectedLayoutId(defaultLayoutFromParameters.id, { saveToProfile: false }); return; } + // It there is a defaultLayout setted but didnt found a layout, show a error to the user + if (appParameters.defaultLayout) { + enqueueSnackbar(t("noDefaultLayoutParameter", { layoutName: appParameters.defaultLayout }), { + variant: "warning", + }); + } + // Retreive the selected layout id from the user's profile. If there's no layout specified // or we can't load it then save and select a default layout const layout = currentLayoutId ? await layoutManager.getLayout(currentLayoutId) : undefined; From 0be4a6cd0c65c4bb2e3658da807fa3cdacf3d854 Mon Sep 17 00:00:00 2001 From: Alexandre Neuwald Date: Thu, 19 Dec 2024 14:21:06 +0000 Subject: [PATCH 14/14] save to profile true and fix typo --- .../suite-base/src/providers/CurrentLayoutProvider/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx index 93e2f3456d..25fbb2ccff 100644 --- a/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx +++ b/packages/suite-base/src/providers/CurrentLayoutProvider/index.tsx @@ -280,7 +280,7 @@ export default function CurrentLayoutProvider({ return; } - // For some reason, this needs to go befre the setSelectedLayoutId, probably some initialization + // For some reason, this needs to go before the setSelectedLayoutId, probably some initialization const { currentLayoutId } = await getUserProfile(); // Try to load default layouts, before checking to add the fallback "Default". @@ -291,7 +291,7 @@ export default function CurrentLayoutProvider({ // Check if there's a layout specified by app parameter const defaultLayoutFromParameters = layouts.find((l) => l.name === appParameters.defaultLayout); if (defaultLayoutFromParameters) { - await setSelectedLayoutId(defaultLayoutFromParameters.id, { saveToProfile: false }); + await setSelectedLayoutId(defaultLayoutFromParameters.id, { saveToProfile: true }); return; }