Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read CLI parameters #312

Merged
merged 14 commits into from
Dec 19, 2024
24 changes: 21 additions & 3 deletions packages/suite-base/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Expand All @@ -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"));
Expand Down Expand Up @@ -70,8 +73,9 @@ const mockAppConfiguration: IAppConfiguration = {
};

// Helper to render the App with default props
const setup = (overrides: Partial<React.ComponentProps<typeof App>> = {}) => {
const defaultProps = {
const setup = (overrides: Partial<AppProps> = {}) => {
const defaultProps: AppProps = {
appParameters: {},
appConfiguration: mockAppConfiguration,
deepLinks: [],
dataSources: [],
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
78 changes: 42 additions & 36 deletions packages/suite-base/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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";
import EventsProvider from "@lichtblick/suite-base/providers/EventsProvider";
import LayoutManagerProvider from "@lichtblick/suite-base/providers/LayoutManagerProvider";
import ProblemsContextProvider from "@lichtblick/suite-base/providers/ProblemsContextProvider";
Expand Down Expand Up @@ -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: AppParametersInput;
dataSources: IDataSourceFactory[];
deepLinks: string[];
extensionLoaders: readonly ExtensionLoader[];
layoutLoaders: readonly LayoutLoader[];
nativeAppMenu?: INativeAppMenu;
Expand All @@ -70,6 +73,7 @@ function contextMenuHandler(event: MouseEvent) {
export function App(props: AppProps): React.JSX.Element {
const {
appConfiguration,
appParameters = {},
dataSources,
extensionLoaders,
layoutLoaders,
Expand Down Expand Up @@ -104,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(<StudioToastProvider />);
providers.unshift(<StudioLogsSettingsProvider />);

// Problems provider also must come before other, dependent contexts.
providers.unshift(<ProblemsContextProvider />);
providers.unshift(<CurrentLayoutProvider loaders={layoutLoaders} />);
Expand All @@ -117,6 +117,10 @@ export function App(props: AppProps): React.JSX.Element {
const layoutStorage = useMemo(() => new IdbLayoutStorage(), []);
providers.unshift(<LayoutStorageContext.Provider value={layoutStorage} />);

// The toast and logs provider comes first so they are available to all downstream providers
providers.unshift(<StudioToastProvider />);
providers.unshift(<StudioLogsSettingsProvider />);

const MaybeLaunchPreference = enableLaunchPreferenceScreen === true ? LaunchPreference : Fragment;

useEffect(() => {
Expand All @@ -128,36 +132,38 @@ export function App(props: AppProps): React.JSX.Element {

return (
<AppConfigurationContext.Provider value={appConfiguration}>
<ColorSchemeThemeProvider>
{enableGlobalCss && <GlobalCss />}
<CssBaseline>
<ErrorBoundary>
<MaybeLaunchPreference>
<MultiProvider providers={providers}>
<DocumentTitleAdapter />
<SendNotificationToastAdapter />
<DndProvider backend={HTML5Backend}>
<Suspense fallback={<></>}>
<PanelCatalogProvider>
<Workspace
deepLinks={deepLinks}
appBarLeftInset={props.appBarLeftInset}
onAppBarDoubleClick={props.onAppBarDoubleClick}
showCustomWindowControls={props.showCustomWindowControls}
isMaximized={props.isMaximized}
onMinimizeWindow={props.onMinimizeWindow}
onMaximizeWindow={props.onMaximizeWindow}
onUnmaximizeWindow={props.onUnmaximizeWindow}
onCloseWindow={props.onCloseWindow}
/>
</PanelCatalogProvider>
</Suspense>
</DndProvider>
</MultiProvider>
</MaybeLaunchPreference>
</ErrorBoundary>
</CssBaseline>
</ColorSchemeThemeProvider>
<AppParametersProvider appParameters={appParameters}>
<ColorSchemeThemeProvider>
{enableGlobalCss && <GlobalCss />}
<CssBaseline>
<ErrorBoundary>
<MaybeLaunchPreference>
<MultiProvider providers={providers}>
<DocumentTitleAdapter />
<SendNotificationToastAdapter />
<DndProvider backend={HTML5Backend}>
<Suspense fallback={<></>}>
<PanelCatalogProvider>
<Workspace
deepLinks={deepLinks}
appBarLeftInset={props.appBarLeftInset}
onAppBarDoubleClick={props.onAppBarDoubleClick}
showCustomWindowControls={props.showCustomWindowControls}
isMaximized={props.isMaximized}
onMinimizeWindow={props.onMinimizeWindow}
onMaximizeWindow={props.onMaximizeWindow}
onUnmaximizeWindow={props.onUnmaximizeWindow}
onCloseWindow={props.onCloseWindow}
/>
</PanelCatalogProvider>
</Suspense>
</DndProvider>
</MultiProvider>
</MaybeLaunchPreference>
</ErrorBoundary>
</CssBaseline>
</ColorSchemeThemeProvider>
</AppParametersProvider>
</AppConfigurationContext.Provider>
);
}
7 changes: 7 additions & 0 deletions packages/suite-base/src/AppParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<[email protected]>
// SPDX-License-Identifier: MPL-2.0

export enum AppParametersEnum {
// Used to start with a specific layout passed as parameter
DEFAULT_LAYOUT = "defaultLayout",
}
49 changes: 26 additions & 23 deletions packages/suite-base/src/SharedRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -36,29 +37,31 @@ export function SharedRoot(

return (
<AppConfigurationContext.Provider value={appConfiguration}>
<ColorSchemeThemeProvider>
{enableGlobalCss && <GlobalCss />}
<CssBaseline>
<ErrorBoundary>
<SharedRootContext.Provider
value={{
appBarLeftInset,
AppBarComponent,
appConfiguration,
customWindowControlProps,
dataSources,
deepLinks,
enableLaunchPreferenceScreen,
extensionLoaders,
extraProviders,
onAppBarDoubleClick,
}}
>
{children}
</SharedRootContext.Provider>
</ErrorBoundary>
</CssBaseline>
</ColorSchemeThemeProvider>
<AppParametersProvider>
<ColorSchemeThemeProvider>
{enableGlobalCss && <GlobalCss />}
<CssBaseline>
<ErrorBoundary>
<SharedRootContext.Provider
value={{
appBarLeftInset,
AppBarComponent,
appConfiguration,
customWindowControlProps,
dataSources,
deepLinks,
enableLaunchPreferenceScreen,
extensionLoaders,
extraProviders,
onAppBarDoubleClick,
}}
>
{children}
</SharedRootContext.Provider>
</ErrorBoundary>
</CssBaseline>
</ColorSchemeThemeProvider>
</AppParametersProvider>
</AppConfigurationContext.Provider>
);
}
29 changes: 29 additions & 0 deletions packages/suite-base/src/context/AppParametersContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<[email protected]>
// 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";
import { AppParametersEnum } from "@lichtblick/suite-base/AppParameters";

// Defines a type for input parameters, allowing any string keys with string values.
export type AppParametersInput = Readonly<Record<string, string>>;

// Defines a type for application parameters, restricting keys to the AppParametersEnum for type-safe usage.
export type AppParameters = Readonly<Record<AppParametersEnum, string | undefined>>;

export type AppParametersContext = Immutable<AppParameters>;

export const AppParametersContext = createContext<undefined | AppParametersContext>(undefined);

export function useAppParameters(): AppParameters {
const context = useContext(AppParametersContext);
if (context == undefined) {
throw new Error("useAppParameters must be used within a AppParametersProvider");
}
return context;
}
2 changes: 2 additions & 0 deletions packages/suite-base/src/i18n/en/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
export const general = {
foxglove: "Foxglove",
learnMore: "Learn more",
noDefaultLayoutParameter:
"The layout '{{layoutName}}' specified in the app parameters does not exist.",
};
57 changes: 57 additions & 0 deletions packages/suite-base/src/providers/AppParametersProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/** @jest-environment jsdom */

// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<[email protected]>
// 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 = { defaultLayout: BasicBuilder.string() };
const TestComponent = () => {
const appParameters = useAppParameters();
return <div>{appParameters.defaultLayout}</div>;
};

const { getByText } = render(
<AppParametersProvider appParameters={mockParameters}>
<TestComponent />
</AppParametersProvider>,
);

expect(getByText(mockParameters.defaultLayout)).toBeDefined();
});

it("provides default app parameters when none are given", () => {
const TestComponent = () => {
const appParameters = useAppParameters();
expect(Object.keys(appParameters)).toHaveLength(0);
return <div>{Object.keys(appParameters).length}</div>;
};

const { getByText } = render(
<AppParametersProvider>
<TestComponent />
</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 <div />;
};

expect(() => render(<TestComponent />)).toThrow();
consoleErrorSpy.mockRestore();
});
});
37 changes: 37 additions & 0 deletions packages/suite-base/src/providers/AppParametersProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: Copyright (C) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)<[email protected]>
// 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,
AppParametersInput,
} from "@lichtblick/suite-base/context/AppParametersContext";

type Props = PropsWithChildren<{
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: AppParameters = useMemo(() => appParameters, [appParameters]);
return (
<AppParametersContext.Provider value={parameters}>{children}</AppParametersContext.Provider>
);
}
Loading
Loading