From add29cc0f0c46a879802323e820662b6dbd71135 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Mon, 24 Jun 2024 16:28:06 +0200 Subject: [PATCH 1/7] Allow the user to link settings to message converters depending on the panel type and the message type --- .../suite-base/src/PanelAPI/useConfigById.ts | 18 ++++++- .../PanelExtensionAdapter.tsx | 52 ++++++++++++++----- .../messageProcessing.ts | 1 + .../PanelExtensionAdapter/renderState.ts | 24 +++++++-- .../src/components/PanelSettings/index.tsx | 29 +++++++++-- .../src/context/ExtensionCatalogContext.ts | 11 ++++ packages/suite/src/index.ts | 9 ++++ 7 files changed, 124 insertions(+), 20 deletions(-) diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index 0980c6c9cd..30463fce27 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -5,6 +5,7 @@ // 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 * as _ from "lodash-es"; import { useCallback } from "react"; import { DeepPartial } from "ts-essentials"; @@ -13,6 +14,10 @@ import { useCurrentLayoutActions, useCurrentLayoutSelector, } from "@lichtblick/suite-base/context/CurrentLayoutContext"; +import { + getExtensionPanelSettings, + useExtensionCatalog, +} from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { SaveConfig } from "@lichtblick/suite-base/types/panels"; import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; @@ -24,15 +29,24 @@ export default function useConfigById>( panelId: string | undefined, ): [Config | undefined, SaveConfig] { const { getCurrentLayoutState, savePanelConfigs } = useCurrentLayoutActions(); + const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); const configSelector = useCallback( (state: DeepPartial) => { if (panelId == undefined) { return undefined; } - return maybeCast(state.selectedLayout?.data?.configById?.[panelId]); + const stateConfig = maybeCast(state.selectedLayout?.data?.configById?.[panelId]); + const topics = Object.keys(stateConfig?.topics ?? {}); + const topicsSettings = _.merge( + {}, + ...topics.map((topic) => ({ [topic]: extensionSettings[topic]?.defaultConfig })), + stateConfig?.topics, + ); + + return maybeCast({ ...stateConfig, topics: topicsSettings }); }, - [panelId], + [panelId, extensionSettings], ); const config = useCurrentLayoutSelector(configSelector); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx index e65ea29975..9d42bc4df1 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx +++ b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx @@ -7,6 +7,7 @@ import { fromSec, toSec } from "@foxglove/rostime"; import { useTheme } from "@mui/material"; +import { produce } from "immer"; import { CSSProperties, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useLatest } from "react-use"; import { v4 as uuid } from "uuid"; @@ -20,6 +21,7 @@ import { ParameterValue, RenderState, SettingsTree, + SettingsTreeAction, Subscription, Time, VariableValue, @@ -34,6 +36,7 @@ import PanelToolbar from "@lichtblick/suite-base/components/PanelToolbar"; import { useAppConfiguration } from "@lichtblick/suite-base/context/AppConfigurationContext"; import { ExtensionCatalog, + getExtensionPanelSettings, useExtensionCatalog, } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { @@ -56,9 +59,10 @@ import { PanelConfig, SaveConfig } from "@lichtblick/suite-base/types/panels"; import { assertNever } from "@lichtblick/suite-base/util/assertNever"; import { PanelConfigVersionError } from "./PanelConfigVersionError"; -import { initRenderStateBuilder } from "./renderState"; +import { RenderStateConfig, initRenderStateBuilder } from "./renderState"; import { BuiltinPanelExtensionContext } from "./types"; import { useSharedPanelState } from "./useSharedPanelState"; +import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; const log = Logger.getLogger(__filename); @@ -115,7 +119,7 @@ function PanelExtensionAdapter( // // We store the config in a ref to avoid re-initializing the panel when the react config // changes. - const initialState = useLatest(config); + const initialState = useLatest(maybeCast(config)); const messagePipelineContext = useMessagePipeline(selectContext); @@ -124,7 +128,7 @@ function PanelExtensionAdapter( const { capabilities, profile: dataSourceProfile, presence: playerPresence } = playerState; - const { openSiblingPanel, setMessagePathDropConfig } = usePanelContext(); + const { openSiblingPanel, setMessagePathDropConfig, type: panelName } = usePanelContext(); const [panelId] = useState(() => uuid()); const isMounted = useSynchronousMountedState(); @@ -240,6 +244,7 @@ function PanelExtensionAdapter( sortedTopics, subscriptions: localSubscriptions, watchedFields, + config: initialState.current, }); if (!renderState) { @@ -288,10 +293,13 @@ function PanelExtensionAdapter( sharedPanelState, sortedTopics, watchedFields, + initialState, ]); const updatePanelSettingsTree = usePanelSettingsTreeUpdate(); + const extensionsSettings = useExtensionCatalog(getExtensionPanelSettings); + type PartialPanelExtensionContext = Omit; const partialExtensionContext = useMemo(() => { @@ -314,6 +322,20 @@ function PanelExtensionAdapter( }, }; + const extensionSettingsActionHandler = (action: SettingsTreeAction) => { + const { + payload: { path }, + } = action; + + saveConfig( + produce<{ topics: Record }>((draft) => { + if (path[0] === "topics" && path[1] != undefined) { + extensionsSettings[panelName]?.[path[1]]?.handler(action, draft.topics[path[1]]); + } + }), + ); + }; + return { initialState: initialState.current, @@ -505,7 +527,11 @@ function PanelExtensionAdapter( if (!isMounted()) { return; } - updatePanelSettingsTree(settings); + const actionHandler: typeof settings.actionHandler = (action) => { + settings.actionHandler(action); + extensionSettingsActionHandler(action); + }; + updatePanelSettingsTree({ ...settings, actionHandler }); }, setDefaultPanelTitle: (title: string) => { @@ -522,22 +548,24 @@ function PanelExtensionAdapter( // Disable this rule because the metadata function. If used, it will break. // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - capabilities, - clearHoverValue, - dataSourceProfile, - getMessagePipelineContext, initialState, + seekPlayback, + dataSourceProfile, + setSharedPanelState, + capabilities, isMounted, openSiblingPanel, - panelId, saveConfig, - seekPlayback, - setDefaultPanelTitle, + extensionsSettings, + panelName, + getMessagePipelineContext, setGlobalVariables, + clearHoverValue, setHoverValue, - setSharedPanelState, setSubscriptions, + panelId, updatePanelSettingsTree, + setDefaultPanelTitle, setMessagePathDropConfig, ]); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/messageProcessing.ts b/packages/suite-base/src/components/PanelExtensionAdapter/messageProcessing.ts index 230f2ef0cd..558c2c8a0a 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/messageProcessing.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/messageProcessing.ts @@ -58,6 +58,7 @@ export function convertMessage( message: convertedMessage, originalMessageEvent: messageEvent, sizeInBytes: messageEvent.sizeInBytes, + topicConfig: messageEvent.topicConfig, }); } } diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts index e4dcb864e9..306e0d27f7 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts @@ -41,6 +41,10 @@ import { const EmptyParameters = new Map(); +export type RenderStateConfig = { + topics: Record; +}; + export type BuilderRenderStateInput = Immutable<{ appSettings: Map | undefined; colorScheme: RenderState["colorScheme"] | undefined; @@ -53,6 +57,7 @@ export type BuilderRenderStateInput = Immutable<{ sortedTopics: readonly PlayerTopic[]; subscriptions: Subscription[]; watchedFields: Set; + config: RenderStateConfig | undefined; }>; type BuildRenderStateFn = (input: BuilderRenderStateInput) => Immutable | undefined; @@ -98,6 +103,7 @@ function initRenderStateBuilder(): BuildRenderStateFn { sortedTopics, subscriptions, watchedFields, + config, } = input; // Should render indicates whether any fields of render state are updated @@ -204,7 +210,11 @@ function initRenderStateBuilder(): BuildRenderStateFn { if (unconvertedSubscriptionTopics.has(messageEvent.topic)) { postProcessedFrame.push(messageEvent); } - convertMessage(messageEvent, topicSchemaConverters, postProcessedFrame); + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + topicSchemaConverters, + postProcessedFrame, + ); lastMessageByTopic.set(messageEvent.topic, messageEvent); } renderState.currentFrame = postProcessedFrame; @@ -214,7 +224,11 @@ function initRenderStateBuilder(): BuildRenderStateFn { // only the new conversions on our most recent message on each topic. const postProcessedFrame: MessageEvent[] = []; for (const messageEvent of lastMessageByTopic.values()) { - convertMessage(messageEvent, newConverters, postProcessedFrame); + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + newConverters, + postProcessedFrame, + ); } renderState.currentFrame = postProcessedFrame; shouldRender = true; @@ -262,7 +276,11 @@ function initRenderStateBuilder(): BuildRenderStateFn { if (unconvertedSubscriptionTopics.has(messageEvent.topic)) { frames.push(messageEvent); } - convertMessage(messageEvent, topicSchemaConverters, frames); + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + topicSchemaConverters, + frames, + ); }, ); } diff --git a/packages/suite-base/src/components/PanelSettings/index.tsx b/packages/suite-base/src/components/PanelSettings/index.tsx index a1be6d2a88..dfd9f9b813 100644 --- a/packages/suite-base/src/components/PanelSettings/index.tsx +++ b/packages/suite-base/src/components/PanelSettings/index.tsx @@ -6,6 +6,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import { Divider, Typography } from "@mui/material"; +import * as _ from "lodash-es"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useUnmount } from "react-use"; @@ -25,6 +26,10 @@ import { useCurrentLayoutSelector, useSelectedPanels, } from "@lichtblick/suite-base/context/CurrentLayoutContext"; +import { + getExtensionPanelSettings, + useExtensionCatalog, +} from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { usePanelCatalog } from "@lichtblick/suite-base/context/PanelCatalogContext"; import { PanelStateStore, @@ -34,6 +39,7 @@ import { useAppConfigurationValue } from "@lichtblick/suite-base/hooks"; import { PanelConfig } from "@lichtblick/suite-base/types/panels"; import { TAB_PANEL_TYPE } from "@lichtblick/suite-base/util/globalConstants"; import { getPanelTypeFromId } from "@lichtblick/suite-base/util/layout"; +import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; const singlePanelIdSelector = (state: LayoutState) => typeof state.selectedLayout?.data?.layout === "string" @@ -146,9 +152,26 @@ export default function PanelSettings({ const [config] = useConfigById(selectedPanelId); - const settingsTree = usePanelStateStore((state) => - selectedPanelId ? state.settingsTrees[selectedPanelId] : undefined, - ); + const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); + + const settingsTree = usePanelStateStore((state) => { + if (selectedPanelId) { + const set = state.settingsTrees[selectedPanelId]; + if (set && panelType) { + const topics = Object.keys(set.nodes.topics?.children ?? {}); + const topicsConfig = maybeCast<{ topics: Record }>(config)?.topics; + const topicsSettings = _.merge( + {}, + ...topics.map((topic) => ({ + [topic]: extensionSettings[panelType]?.[topic]?.settings(topicsConfig?.[topic]), + })), + ); + + return { ...set, nodes: _.merge({}, set.nodes, { topics: { children: topicsSettings } }) }; + } + } + return undefined; + }); const resetToDefaults = useCallback(() => { if (selectedPanelId) { diff --git a/packages/suite-base/src/context/ExtensionCatalogContext.ts b/packages/suite-base/src/context/ExtensionCatalogContext.ts index 8eddf3700c..01921c2d87 100644 --- a/packages/suite-base/src/context/ExtensionCatalogContext.ts +++ b/packages/suite-base/src/context/ExtensionCatalogContext.ts @@ -5,6 +5,7 @@ // 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 * as _ from "lodash-es"; import { createContext } from "react"; import { StoreApi, useStore } from "zustand"; @@ -12,6 +13,7 @@ import { useGuaranteedContext } from "@lichtblick/hooks"; import { ExtensionPanelRegistration, Immutable, + PanelSettings, RegisterMessageConverterArgs, } from "@lichtblick/suite"; import { TopicAliasFunctions } from "@lichtblick/suite-base/players/TopicAliasingPlayer/TopicAliasingPlayer"; @@ -46,3 +48,12 @@ export function useExtensionCatalog(selector: (registry: ExtensionCatalog) => const context = useGuaranteedContext(ExtensionCatalogContext); return useStore(context, selector); } + +export function getExtensionPanelSettings( + reg: ExtensionCatalog, +): Record>> { + return _.merge( + {}, + ...(reg.installedMessageConverters ?? []).map((converter) => converter.panelSettings), + ); +} diff --git a/packages/suite/src/index.ts b/packages/suite/src/index.ts index abd87208c4..6c4193af47 100644 --- a/packages/suite/src/index.ts +++ b/packages/suite/src/index.ts @@ -148,6 +148,8 @@ export type MessageEvent = { * un-converted message event. */ originalMessageEvent?: MessageEvent; + + topicConfig?: unknown; }; export interface LayoutActions { @@ -440,10 +442,17 @@ export type ExtensionPanelRegistration = { initPanel: (context: PanelExtensionContext) => void | (() => void); }; +export interface PanelSettings { + settings: (config?: ExtensionSettings) => SettingsTreeNode; + handler: (action: SettingsTreeAction, config?: ExtensionSettings) => void; + defaultConfig?: ExtensionSettings; +} + export type RegisterMessageConverterArgs = { fromSchemaName: string; toSchemaName: string; converter: (msg: Src, event: Immutable>) => unknown; + panelSettings?: Record>>; }; type BaseTopic = { name: string; schemaName?: string }; From f330f55ffa09df43f940638e987109b7a4f2d412 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Mon, 1 Jul 2024 11:48:07 +0200 Subject: [PATCH 2/7] Map converters settings api in the extension by panel name only Use the fromSchemaName property to find the concerned topic --- .../suite-base/src/PanelAPI/useConfigById.ts | 17 ++++++- .../src/components/MessagePipeline/index.tsx | 5 ++ .../PanelExtensionAdapter/renderState.ts | 47 +++++++++++++------ .../src/components/PanelSettings/index.tsx | 18 +++++-- .../src/context/ExtensionCatalogContext.ts | 11 ++--- .../providers/ExtensionCatalogProvider.tsx | 17 +++++++ packages/suite/src/index.ts | 2 +- 7 files changed, 90 insertions(+), 27 deletions(-) diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index 30463fce27..c793144d6a 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -9,6 +9,10 @@ import * as _ from "lodash-es"; import { useCallback } from "react"; import { DeepPartial } from "ts-essentials"; +import { + getTopicToSchemaNameMap, + useMessagePipeline, +} from "@foxglove/studio-base/components/MessagePipeline"; import { LayoutState, useCurrentLayoutActions, @@ -30,6 +34,7 @@ export default function useConfigById>( ): [Config | undefined, SaveConfig] { const { getCurrentLayoutState, savePanelConfigs } = useCurrentLayoutActions(); const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); + const topicToSchemaNameMap = useMessagePipeline(getTopicToSchemaNameMap); const configSelector = useCallback( (state: DeepPartial) => { @@ -40,13 +45,21 @@ export default function useConfigById>( const topics = Object.keys(stateConfig?.topics ?? {}); const topicsSettings = _.merge( {}, - ...topics.map((topic) => ({ [topic]: extensionSettings[topic]?.defaultConfig })), + ...topics.map((topic) => { + const schemaName = topicToSchemaNameMap[topic]; + if (schemaName == undefined) { + return {}; + } + return { + [topic]: extensionSettings[schemaName]?.defaultConfig, + }; + }), stateConfig?.topics, ); return maybeCast({ ...stateConfig, topics: topicsSettings }); }, - [panelId, extensionSettings], + [panelId, extensionSettings, topicToSchemaNameMap], ); const config = useCurrentLayoutSelector(configSelector); diff --git a/packages/suite-base/src/components/MessagePipeline/index.tsx b/packages/suite-base/src/components/MessagePipeline/index.tsx index 2cc7e5a2b4..743426d0a8 100644 --- a/packages/suite-base/src/components/MessagePipeline/index.tsx +++ b/packages/suite-base/src/components/MessagePipeline/index.tsx @@ -338,3 +338,8 @@ function createPlayerListener(args: { }, }; } + +export const getTopicToSchemaNameMap = ( + state: MessagePipelineContext, +): Record => + _.mapValues(_.keyBy(state.sortedTopics, "name"), ({ schemaName }) => schemaName); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts index 306e0d27f7..d73cc8f86a 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts @@ -6,6 +6,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import { compare, toSec } from "@foxglove/rostime"; +import * as _ from "lodash-es"; import memoizeWeak from "memoize-weak"; import { Writable } from "ts-essentials"; @@ -106,6 +107,11 @@ function initRenderStateBuilder(): BuildRenderStateFn { config, } = input; + const topicToSchemaNameMap = _.mapValues( + _.keyBy(sortedTopics, "name"), + ({ schemaName }) => schemaName, + ); + // Should render indicates whether any fields of render state are updated let shouldRender = false; @@ -210,11 +216,15 @@ function initRenderStateBuilder(): BuildRenderStateFn { if (unconvertedSubscriptionTopics.has(messageEvent.topic)) { postProcessedFrame.push(messageEvent); } - convertMessage( - { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, - topicSchemaConverters, - postProcessedFrame, - ); + + const schemaName = topicToSchemaNameMap[messageEvent.topic]; + if (schemaName) { + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + topicSchemaConverters, + postProcessedFrame, + ); + } lastMessageByTopic.set(messageEvent.topic, messageEvent); } renderState.currentFrame = postProcessedFrame; @@ -224,11 +234,14 @@ function initRenderStateBuilder(): BuildRenderStateFn { // only the new conversions on our most recent message on each topic. const postProcessedFrame: MessageEvent[] = []; for (const messageEvent of lastMessageByTopic.values()) { - convertMessage( - { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, - newConverters, - postProcessedFrame, - ); + const schemaName = topicToSchemaNameMap[messageEvent.topic]; + if (schemaName) { + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + newConverters, + postProcessedFrame, + ); + } } renderState.currentFrame = postProcessedFrame; shouldRender = true; @@ -276,11 +289,15 @@ function initRenderStateBuilder(): BuildRenderStateFn { if (unconvertedSubscriptionTopics.has(messageEvent.topic)) { frames.push(messageEvent); } - convertMessage( - { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, - topicSchemaConverters, - frames, - ); + + const schemaName = topicToSchemaNameMap[messageEvent.topic]; + if (schemaName) { + convertMessage( + { ...messageEvent, topicConfig: config?.topics[messageEvent.topic] }, + topicSchemaConverters, + frames, + ); + } }, ); } diff --git a/packages/suite-base/src/components/PanelSettings/index.tsx b/packages/suite-base/src/components/PanelSettings/index.tsx index dfd9f9b813..6d9447f739 100644 --- a/packages/suite-base/src/components/PanelSettings/index.tsx +++ b/packages/suite-base/src/components/PanelSettings/index.tsx @@ -15,6 +15,10 @@ import { SettingsTree } from "@lichtblick/suite"; import { AppSetting } from "@lichtblick/suite-base/AppSetting"; import { useConfigById } from "@lichtblick/suite-base/PanelAPI"; import EmptyState from "@lichtblick/suite-base/components/EmptyState"; +import { + getTopicToSchemaNameMap, + useMessagePipeline, +} from "@lichtblick/suite-base/components/MessagePipeline"; import { ActionMenu } from "@lichtblick/suite-base/components/PanelSettings/ActionMenu"; import SettingsTreeEditor from "@lichtblick/suite-base/components/SettingsTreeEditor"; import { ShareJsonModal } from "@lichtblick/suite-base/components/ShareJsonModal"; @@ -154,6 +158,8 @@ export default function PanelSettings({ const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); + const topicToSchemaNameMap = useMessagePipeline(getTopicToSchemaNameMap); + const settingsTree = usePanelStateStore((state) => { if (selectedPanelId) { const set = state.settingsTrees[selectedPanelId]; @@ -162,9 +168,15 @@ export default function PanelSettings({ const topicsConfig = maybeCast<{ topics: Record }>(config)?.topics; const topicsSettings = _.merge( {}, - ...topics.map((topic) => ({ - [topic]: extensionSettings[panelType]?.[topic]?.settings(topicsConfig?.[topic]), - })), + ...topics.map((topic) => { + const schemaName = topicToSchemaNameMap[topic]; + if (schemaName == undefined) { + return {}; + } + return { + [topic]: extensionSettings[panelType]?.[schemaName]?.settings(topicsConfig?.[topic]), + }; + }), ); return { ...set, nodes: _.merge({}, set.nodes, { topics: { children: topicsSettings } }) }; diff --git a/packages/suite-base/src/context/ExtensionCatalogContext.ts b/packages/suite-base/src/context/ExtensionCatalogContext.ts index 01921c2d87..c84827ea88 100644 --- a/packages/suite-base/src/context/ExtensionCatalogContext.ts +++ b/packages/suite-base/src/context/ExtensionCatalogContext.ts @@ -5,7 +5,6 @@ // 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 * as _ from "lodash-es"; import { createContext } from "react"; import { StoreApi, useStore } from "zustand"; @@ -36,8 +35,11 @@ export type ExtensionCatalog = Immutable<{ installedExtensions: undefined | ExtensionInfo[]; installedPanels: undefined | Record; - installedMessageConverters: undefined | RegisterMessageConverterArgs[]; + installedMessageConverters: + | undefined + | Omit, "panelSettings">[]; installedTopicAliasFunctions: undefined | TopicAliasFunctions; + panelSettings: undefined | Record>>; }>; export const ExtensionCatalogContext = createContext>( @@ -52,8 +54,5 @@ export function useExtensionCatalog(selector: (registry: ExtensionCatalog) => export function getExtensionPanelSettings( reg: ExtensionCatalog, ): Record>> { - return _.merge( - {}, - ...(reg.installedMessageConverters ?? []).map((converter) => converter.panelSettings), - ); + return reg.panelSettings ?? {}; } diff --git a/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx b/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx index 4f4b545db7..f5952166ad 100644 --- a/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx +++ b/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx @@ -5,6 +5,7 @@ // 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 * as _ from "lodash-es"; import React, { PropsWithChildren, useEffect, useState } from "react"; import ReactDOM from "react-dom"; import { StoreApi, createStore } from "zustand"; @@ -13,6 +14,7 @@ import Logger from "@lichtblick/log"; import { ExtensionContext, ExtensionModule, + PanelSettings, RegisterMessageConverterArgs, TopicAliasFunction, } from "@lichtblick/suite"; @@ -35,6 +37,7 @@ type ContributionPoints = { panels: Record; messageConverters: MessageConverter[]; topicAliasFunctions: TopicAliasFunctions; + panelSettings: Record>>; }; function activateExtension( @@ -47,6 +50,8 @@ function activateExtension( const messageConverters: RegisterMessageConverterArgs[] = []; + const panelSettings: Record>> = {}; + const topicAliasFunctions: ContributionPoints["topicAliasFunctions"] = []; log.debug(`Activating extension ${extension.qualifiedName}`); @@ -90,6 +95,12 @@ function activateExtension( ...args, extensionNamespace: extension.namespace, } as MessageConverter); + + const converterSettings = _.mapValues(args.panelSettings, (settings) => ({ + [args.fromSchemaName]: settings, + })); + + _.merge(panelSettings, converterSettings); }, registerTopicAliases: (aliasFunction: TopicAliasFunction) => { @@ -114,6 +125,7 @@ function activateExtension( panels, messageConverters, topicAliasFunctions, + panelSettings, }; } @@ -148,6 +160,7 @@ function createExtensionRegistryStore( panels: {}, messageConverters: [], topicAliasFunctions: [], + panelSettings: {}, }; for (const loader of loaders) { try { @@ -157,6 +170,7 @@ function createExtensionRegistryStore( const unwrappedExtensionSource = await loader.loadExtension(extension.id); const contributionPoints = activateExtension(extension, unwrappedExtensionSource); Object.assign(allContributionPoints.panels, contributionPoints.panels); + _.merge(allContributionPoints.panelSettings, contributionPoints.panelSettings); allContributionPoints.messageConverters.push(...contributionPoints.messageConverters); allContributionPoints.topicAliasFunctions.push( ...contributionPoints.topicAliasFunctions, @@ -177,6 +191,7 @@ function createExtensionRegistryStore( installedPanels: allContributionPoints.panels, installedMessageConverters: allContributionPoints.messageConverters, installedTopicAliasFunctions: allContributionPoints.topicAliasFunctions, + panelSettings: allContributionPoints.panelSettings, }); }, @@ -189,6 +204,8 @@ function createExtensionRegistryStore( installedTopicAliasFunctions: [], + panelSettings: {}, + uninstallExtension: async (namespace: ExtensionNamespace, id: string) => { const namespacedLoader = loaders.find((loader) => loader.namespace === namespace); if (namespacedLoader == undefined) { diff --git a/packages/suite/src/index.ts b/packages/suite/src/index.ts index 6c4193af47..0966308775 100644 --- a/packages/suite/src/index.ts +++ b/packages/suite/src/index.ts @@ -452,7 +452,7 @@ export type RegisterMessageConverterArgs = { fromSchemaName: string; toSchemaName: string; converter: (msg: Src, event: Immutable>) => unknown; - panelSettings?: Record>>; + panelSettings?: Record>; }; type BaseTopic = { name: string; schemaName?: string }; From 03ca94b8c9ea2a949b594e3f1ad6ed0f73bbd51e Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Fri, 5 Jul 2024 10:32:34 +0200 Subject: [PATCH 3/7] Fixed existing tests --- .../src/selectWithUnstableIdentityWarning.ts | 2 +- .../suite-base/src/PanelAPI/useConfigById.ts | 25 +++++++++++++++---- .../PanelExtensionAdapter/renderState.ts | 2 +- .../src/components/PanelLayout.test.tsx | 18 ++++++++----- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/hooks/src/selectWithUnstableIdentityWarning.ts b/packages/hooks/src/selectWithUnstableIdentityWarning.ts index 45fe58f105..0ff188fde5 100644 --- a/packages/hooks/src/selectWithUnstableIdentityWarning.ts +++ b/packages/hooks/src/selectWithUnstableIdentityWarning.ts @@ -14,7 +14,7 @@ export function selectWithUnstableIdentityWarning(value: T, selector: (val const secondResult = selector(value); if (result !== secondResult) { log.warn(`Selector ${selector.toString()} produced different values for the same input. - This will cause unecesessery re-renders of your component.`); + This will cause unecesessery re-renders of your component.`); } return secondResult; } diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index c793144d6a..032b667e0c 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -6,7 +6,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import * as _ from "lodash-es"; -import { useCallback } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { DeepPartial } from "ts-essentials"; import { @@ -25,6 +25,8 @@ import { import { SaveConfig } from "@lichtblick/suite-base/types/panels"; import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; +import { getPanelTypeFromId } from "../util/layout"; + /** * Like `useConfig`, but for a specific panel id. This generally shouldn't be used by panels * directly, but is for use in internal code that's running outside of regular context providers. @@ -34,7 +36,13 @@ export default function useConfigById>( ): [Config | undefined, SaveConfig] { const { getCurrentLayoutState, savePanelConfigs } = useCurrentLayoutActions(); const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); - const topicToSchemaNameMap = useMessagePipeline(getTopicToSchemaNameMap); + // const topicToSchemaNameMap = useMessagePipeline(getTopicToSchemaNameMap); + const sortedTopics = useMessagePipeline((state) => state.sortedTopics); + + const topicToSchemaNameMap = useMemo( + () => _.mapValues(_.keyBy(sortedTopics, "name"), ({ schemaName }) => schemaName), + [sortedTopics], + ); const configSelector = useCallback( (state: DeepPartial) => { @@ -43,20 +51,27 @@ export default function useConfigById>( } const stateConfig = maybeCast(state.selectedLayout?.data?.configById?.[panelId]); const topics = Object.keys(stateConfig?.topics ?? {}); - const topicsSettings = _.merge( + const panelType = getPanelTypeFromId(panelId); + const topicsSettings: Record = _.merge( {}, ...topics.map((topic) => { const schemaName = topicToSchemaNameMap[topic]; if (schemaName == undefined) { return {}; } + const defaultConfig = extensionSettings[panelType]?.[schemaName]?.defaultConfig; + if (defaultConfig == undefined) { + return {}; + } return { - [topic]: extensionSettings[schemaName]?.defaultConfig, + [topic]: defaultConfig, }; }), stateConfig?.topics, ); - + if (Object.keys(topicsSettings).length === 0) { + return stateConfig; + } return maybeCast({ ...stateConfig, topics: topicsSettings }); }, [panelId, extensionSettings, topicToSchemaNameMap], diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts index d73cc8f86a..ff78af9407 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.ts @@ -58,7 +58,7 @@ export type BuilderRenderStateInput = Immutable<{ sortedTopics: readonly PlayerTopic[]; subscriptions: Subscription[]; watchedFields: Set; - config: RenderStateConfig | undefined; + config?: RenderStateConfig | undefined; }>; type BuildRenderStateFn = (input: BuilderRenderStateInput) => Immutable | undefined; diff --git a/packages/suite-base/src/components/PanelLayout.test.tsx b/packages/suite-base/src/components/PanelLayout.test.tsx index 1e496c1763..bb3fa4631d 100644 --- a/packages/suite-base/src/components/PanelLayout.test.tsx +++ b/packages/suite-base/src/components/PanelLayout.test.tsx @@ -11,6 +11,7 @@ import { useState } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; +import { MessagePipelineProvider } from "@lichtblick/suite-base/components/MessagePipeline"; import Panel from "@lichtblick/suite-base/components/Panel"; import AppConfigurationContext from "@lichtblick/suite-base/context/AppConfigurationContext"; import PanelCatalogContext, { @@ -18,6 +19,7 @@ import PanelCatalogContext, { PanelInfo, } from "@lichtblick/suite-base/context/PanelCatalogContext"; import MockCurrentLayoutProvider from "@lichtblick/suite-base/providers/CurrentLayoutProvider/MockCurrentLayoutProvider"; +import ExtensionCatalogProvider from "@lichtblick/suite-base/providers/ExtensionCatalogProvider"; import { PanelStateContextProvider } from "@lichtblick/suite-base/providers/PanelStateContextProvider"; import WorkspaceContextProvider from "@lichtblick/suite-base/providers/WorkspaceContextProvider"; import { makeMockAppConfiguration } from "@lichtblick/suite-base/util/makeMockAppConfiguration"; @@ -45,7 +47,7 @@ describe("UnconnectedPanelLayout", () => { }); it("does not remount panels when changing split percentage", async () => { - jest.spyOn(console, "error").mockImplementation(() => undefined); + // jest.spyOn(console, "error").mockImplementation(() => undefined); const renderA = jest.fn().mockReturnValue(<>A); const moduleA = jest.fn().mockResolvedValue({ @@ -87,11 +89,15 @@ describe("UnconnectedPanelLayout", () => { - - - {children} - - + + + + + {children} + + + + From 09c31a3a7bd6e645f8865fb6c485a7e7e3cad383 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Mon, 8 Jul 2024 02:36:54 +0200 Subject: [PATCH 4/7] Adapt extensionCatalogProviders to mocked installed converters Add tests --- .../suite-base/src/PanelAPI/useConfigById.ts | 13 +- .../ExtensionCatalogProvider.test.tsx | 136 +++++++++++++++++- .../providers/ExtensionCatalogProvider.tsx | 7 +- 3 files changed, 144 insertions(+), 12 deletions(-) diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index 032b667e0c..f82b0d9a59 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -36,26 +36,18 @@ export default function useConfigById>( ): [Config | undefined, SaveConfig] { const { getCurrentLayoutState, savePanelConfigs } = useCurrentLayoutActions(); const extensionSettings = useExtensionCatalog(getExtensionPanelSettings); - // const topicToSchemaNameMap = useMessagePipeline(getTopicToSchemaNameMap); const sortedTopics = useMessagePipeline((state) => state.sortedTopics); - const topicToSchemaNameMap = useMemo( - () => _.mapValues(_.keyBy(sortedTopics, "name"), ({ schemaName }) => schemaName), - [sortedTopics], - ); - const configSelector = useCallback( (state: DeepPartial) => { if (panelId == undefined) { return undefined; } const stateConfig = maybeCast(state.selectedLayout?.data?.configById?.[panelId]); - const topics = Object.keys(stateConfig?.topics ?? {}); const panelType = getPanelTypeFromId(panelId); const topicsSettings: Record = _.merge( {}, - ...topics.map((topic) => { - const schemaName = topicToSchemaNameMap[topic]; + ...sortedTopics.map(({ name: topic, schemaName }) => { if (schemaName == undefined) { return {}; } @@ -69,12 +61,13 @@ export default function useConfigById>( }), stateConfig?.topics, ); + // console.log(extensionSettings); if (Object.keys(topicsSettings).length === 0) { return stateConfig; } return maybeCast({ ...stateConfig, topics: topicsSettings }); }, - [panelId, extensionSettings, topicToSchemaNameMap], + [panelId, extensionSettings, sortedTopics], ); const config = useCurrentLayoutSelector(configSelector); diff --git a/packages/suite-base/src/providers/ExtensionCatalogProvider.test.tsx b/packages/suite-base/src/providers/ExtensionCatalogProvider.test.tsx index dbd838497f..a3a5c44680 100644 --- a/packages/suite-base/src/providers/ExtensionCatalogProvider.test.tsx +++ b/packages/suite-base/src/providers/ExtensionCatalogProvider.test.tsx @@ -6,10 +6,15 @@ // 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 { renderHook, waitFor } from "@testing-library/react"; +import { render, renderHook, waitFor } from "@testing-library/react"; +import { useEffect } from "react"; +import { PanelSettings } from "@lichtblick/suite"; +import { useConfigById } from "@lichtblick/suite-base/PanelAPI"; +import Panel from "@lichtblick/suite-base/components/Panel"; import { useExtensionCatalog } from "@lichtblick/suite-base/context/ExtensionCatalogContext"; import { ExtensionLoader } from "@lichtblick/suite-base/services/ExtensionLoader"; +import PanelSetup from "@lichtblick/suite-base/stories/PanelSetup"; import { ExtensionInfo } from "@lichtblick/suite-base/types/Extensions"; import ExtensionCatalogProvider from "./ExtensionCatalogProvider"; @@ -197,6 +202,70 @@ describe("ExtensionCatalogProvider", () => { ]); }); + it("should register panel settings", async () => { + const source = ` + module.exports = { + activate: function(ctx) { + ctx.registerMessageConverter({ + fromSchemaName: "from.Schema", + toSchemaName: "to.Schema", + converter: (msg) => msg, + panelSettings: { + Dummy: { + settings: (config) => ({ + fields: { + test: { + input: "boolean", + value: config?.test, + label: "Nope", + }, + }, + }), + handler: () => {}, + defaultConfig: { + test: true, + }, + }, + }, + }); + } + } + `; + + const loadExtension = jest.fn().mockResolvedValue(source); + const mockPrivateLoader: ExtensionLoader = { + namespace: "org", + getExtensions: jest + .fn() + .mockResolvedValue([fakeExtension({ namespace: "org", name: "sample", version: "1" })]), + loadExtension, + installExtension: jest.fn(), + uninstallExtension: jest.fn(), + }; + + const { result } = renderHook(() => useExtensionCatalog((state) => state), { + initialProps: {}, + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await waitFor(() => { + expect(loadExtension).toHaveBeenCalledTimes(1); + }); + expect(result.current.panelSettings).toEqual({ + Dummy: { + "from.Schema": { + defaultConfig: { test: true }, + handler: expect.any(Function), + settings: expect.any(Function), + }, + }, + }); + }); + it("should register topic aliases", async () => { const source = ` module.exports = { @@ -235,4 +304,69 @@ describe("ExtensionCatalogProvider", () => { { extensionId: "id", aliasFunction: expect.any(Function) }, ]); }); + + it("should register a default config", async () => { + jest.spyOn(console, "error").mockImplementation(() => {}); + + function getDummyPanel(updatedConfig: jest.Mock, childId: string) { + function DummyComponent(): ReactNull { + const [config] = useConfigById(childId); + + useEffect(() => updatedConfig(config), [config]); + return ReactNull; + } + DummyComponent.panelType = "Dummy"; + DummyComponent.defaultConfig = { someString: "hello world" }; + return Panel(DummyComponent); + } + + const updatedConfig = jest.fn(); + const childId = "Dummy!1my2ydk"; + const DummyPanel = getDummyPanel(updatedConfig, childId); + const generatePanelSettings = (obj: PanelSettings) => obj as PanelSettings; + + render( + msg, + panelSettings: { + Dummy: generatePanelSettings({ + settings: (config) => ({ + fields: { + test: { + input: "boolean", + value: config?.test, + label: "Nope", + }, + }, + }), + handler: () => {}, + defaultConfig: { + test: true, + }, + }), + }, + }, + ], + }} + > + + , + ); + + await waitFor(() => { + expect(updatedConfig).toHaveBeenCalled(); + }); + + expect(updatedConfig.mock.calls.at(-1)).toEqual([ + { someString: "hello world", topics: { myTopic: { test: true } } }, + ]); + + (console.error as jest.Mock).mockRestore(); + }); }); diff --git a/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx b/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx index f5952166ad..78f442a342 100644 --- a/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx +++ b/packages/suite-base/src/providers/ExtensionCatalogProvider.tsx @@ -204,7 +204,12 @@ function createExtensionRegistryStore( installedTopicAliasFunctions: [], - panelSettings: {}, + panelSettings: _.merge( + {}, + ...(mockMessageConverters ?? []).map(({ fromSchemaName, panelSettings }) => + _.mapValues(panelSettings, (settings) => ({ [fromSchemaName]: settings })), + ), + ), uninstallExtension: async (namespace: ExtensionNamespace, id: string) => { const namespacedLoader = loaders.find((loader) => loader.namespace === namespace); From bf8a45e45d2f45d61da6e90685aa02372069f716 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Mon, 8 Jul 2024 12:56:56 +0200 Subject: [PATCH 5/7] Test renderState --- .../PanelExtensionAdapter/renderState.test.ts | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts index 6347eadfee..3dbb0ae55d 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts @@ -957,7 +957,7 @@ describe("renderState", () => { const state2 = buildRenderState( produce(initialState, (draft) => { draft.currentFrame = undefined; - draft.playerState!.progress.messageCache = undefined; + draft.playerState.progress.messageCache = undefined; draft.subscriptions.push({ topic: "test", convertTo: "anotherSchema", preload: true }); }), ); @@ -1067,4 +1067,61 @@ describe("renderState", () => { expect(state).toEqual(undefined); } }); + + it("should add extension settings to converter method", async () => { + const generatePanelSettings = (obj: PanelSettings) => obj as PanelSettings; + const checkRenderedConfig = jest.fn(); + const buildRenderState = initRenderStateBuilder(); + const state = buildRenderState({ + appSettings: undefined, + playerState: undefined, + currentFrame: [ + { + schemaName: "from.Schema", + topic: "myTopic", + receiveTime: { sec: 0, nsec: 0 }, + message: {}, + sizeInBytes: 0, + }, + ], + colorScheme: undefined, + globalVariables: {}, + hoverValue: undefined, + sharedPanelState: undefined, + sortedTopics: [{ name: "myTopic", schemaName: "from.Schema" }], + subscriptions: [{ topic: "myTopic", convertTo: "to.Schema", preload: true }], + watchedFields: new Set(["topics", "currentFrame"]), + messageConverters: [ + { + fromSchemaName: "from.Schema", + toSchemaName: "to.Schema", + converter: (msg, event) => { + checkRenderedConfig(event.topicConfig); + return msg; + }, + panelSettings: { + Dummy: generatePanelSettings({ + settings: (config) => ({ + fields: { + test: { + input: "boolean", + value: config?.test, + label: "Nope", + }, + }, + }), + handler: () => {}, + defaultConfig: { + test: true, + }, + }), + }, + }, + ], + config: { topics: { myTopic: { test: false } } }, + }); + + expect(checkRenderedConfig).toHaveBeenCalled(); + expect(checkRenderedConfig.mock.calls.at(-1)).toEqual([{ test: false }]); + }); }); From 08b42f05a63b7cb671a7984e1edf9c51211e15c5 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Mon, 8 Jul 2024 16:49:00 +0200 Subject: [PATCH 6/7] Apply fixes for the CI Add comments Rename a few variables --- packages/suite-base/src/PanelAPI/useConfigById.ts | 14 +++++--------- .../PanelExtensionAdapter.tsx | 5 +++-- .../PanelExtensionAdapter/renderState.test.ts | 2 +- packages/suite/src/index.ts | 13 +++++++++++++ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index f82b0d9a59..e218055a74 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -6,13 +6,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import * as _ from "lodash-es"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback } from "react"; import { DeepPartial } from "ts-essentials"; -import { - getTopicToSchemaNameMap, - useMessagePipeline, -} from "@foxglove/studio-base/components/MessagePipeline"; +import { useMessagePipeline } from "@foxglove/studio-base/components/MessagePipeline"; import { LayoutState, useCurrentLayoutActions, @@ -45,7 +42,7 @@ export default function useConfigById>( } const stateConfig = maybeCast(state.selectedLayout?.data?.configById?.[panelId]); const panelType = getPanelTypeFromId(panelId); - const topicsSettings: Record = _.merge( + const customSettingsByTopic: Record = _.merge( {}, ...sortedTopics.map(({ name: topic, schemaName }) => { if (schemaName == undefined) { @@ -61,11 +58,10 @@ export default function useConfigById>( }), stateConfig?.topics, ); - // console.log(extensionSettings); - if (Object.keys(topicsSettings).length === 0) { + if (Object.keys(customSettingsByTopic).length === 0) { return stateConfig; } - return maybeCast({ ...stateConfig, topics: topicsSettings }); + return maybeCast({ ...stateConfig, topics: customSettingsByTopic }); }, [panelId, extensionSettings, sortedTopics], ); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx index 9d42bc4df1..c4787f5a68 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx +++ b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx @@ -329,8 +329,9 @@ function PanelExtensionAdapter( saveConfig( produce<{ topics: Record }>((draft) => { - if (path[0] === "topics" && path[1] != undefined) { - extensionsSettings[panelName]?.[path[1]]?.handler(action, draft.topics[path[1]]); + const [category, topicName] = path; + if (category === "topics" && topicName != undefined) { + extensionsSettings[panelName]?.[topicName]?.handler(action, draft.topics[topicName]); } }), ); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts index 3dbb0ae55d..43e36751b5 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts @@ -1072,7 +1072,7 @@ describe("renderState", () => { const generatePanelSettings = (obj: PanelSettings) => obj as PanelSettings; const checkRenderedConfig = jest.fn(); const buildRenderState = initRenderStateBuilder(); - const state = buildRenderState({ + buildRenderState({ appSettings: undefined, playerState: undefined, currentFrame: [ diff --git a/packages/suite/src/index.ts b/packages/suite/src/index.ts index 0966308775..bdb7616632 100644 --- a/packages/suite/src/index.ts +++ b/packages/suite/src/index.ts @@ -443,7 +443,17 @@ export type ExtensionPanelRegistration = { }; export interface PanelSettings { + /** + * @param config value of the custom settings. It's type is the type of the object defined in the *defaultConfig* property + * @returns + * a settings tree node defined as it would be defined in an extension. + * That node will be merged with the node belonging to the concerned topic (path = ["topics", "__topic_name__"]) + */ settings: (config?: ExtensionSettings) => SettingsTreeNode; + /** + * Simple settings handler run right after the default handler for settings. + * @param config is mutated, modifying it allows the state value to be modified and then sent to the converter + */ handler: (action: SettingsTreeAction, config?: ExtensionSettings) => void; defaultConfig?: ExtensionSettings; } @@ -452,6 +462,9 @@ export type RegisterMessageConverterArgs = { fromSchemaName: string; toSchemaName: string; converter: (msg: Src, event: Immutable>) => unknown; + /** + * Custom settings for the topics using the schema specified in the *toSchemaName* property + */ panelSettings?: Record>; }; From e5cde85898c10b3cba50c7be9352f42606e1d174 Mon Sep 17 00:00:00 2001 From: Franck Labracherie Date: Sun, 28 Jul 2024 22:43:25 +0200 Subject: [PATCH 7/7] Move and change messagePipeline selector that maps schemas by topic name --- .../src/selectWithUnstableIdentityWarning.ts | 2 +- .../suite-base/src/PanelAPI/useConfigById.ts | 2 +- .../src/components/MessagePipeline/index.tsx | 5 --- .../MessagePipeline/selectors.test.ts | 43 +++++++++++++++++++ .../components/MessagePipeline/selectors.ts | 15 +++++++ .../PanelExtensionAdapter.tsx | 2 +- .../PanelExtensionAdapter/renderState.test.ts | 3 +- .../src/components/PanelSettings/index.tsx | 6 +-- 8 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 packages/suite-base/src/components/MessagePipeline/selectors.test.ts create mode 100644 packages/suite-base/src/components/MessagePipeline/selectors.ts diff --git a/packages/hooks/src/selectWithUnstableIdentityWarning.ts b/packages/hooks/src/selectWithUnstableIdentityWarning.ts index 0ff188fde5..45fe58f105 100644 --- a/packages/hooks/src/selectWithUnstableIdentityWarning.ts +++ b/packages/hooks/src/selectWithUnstableIdentityWarning.ts @@ -14,7 +14,7 @@ export function selectWithUnstableIdentityWarning(value: T, selector: (val const secondResult = selector(value); if (result !== secondResult) { log.warn(`Selector ${selector.toString()} produced different values for the same input. - This will cause unecesessery re-renders of your component.`); + This will cause unecesessery re-renders of your component.`); } return secondResult; } diff --git a/packages/suite-base/src/PanelAPI/useConfigById.ts b/packages/suite-base/src/PanelAPI/useConfigById.ts index e218055a74..645f04dcea 100644 --- a/packages/suite-base/src/PanelAPI/useConfigById.ts +++ b/packages/suite-base/src/PanelAPI/useConfigById.ts @@ -9,7 +9,7 @@ import * as _ from "lodash-es"; import { useCallback } from "react"; import { DeepPartial } from "ts-essentials"; -import { useMessagePipeline } from "@foxglove/studio-base/components/MessagePipeline"; +import { useMessagePipeline } from "@lichtblick/suite-base/components/MessagePipeline"; import { LayoutState, useCurrentLayoutActions, diff --git a/packages/suite-base/src/components/MessagePipeline/index.tsx b/packages/suite-base/src/components/MessagePipeline/index.tsx index 743426d0a8..2cc7e5a2b4 100644 --- a/packages/suite-base/src/components/MessagePipeline/index.tsx +++ b/packages/suite-base/src/components/MessagePipeline/index.tsx @@ -338,8 +338,3 @@ function createPlayerListener(args: { }, }; } - -export const getTopicToSchemaNameMap = ( - state: MessagePipelineContext, -): Record => - _.mapValues(_.keyBy(state.sortedTopics, "name"), ({ schemaName }) => schemaName); diff --git a/packages/suite-base/src/components/MessagePipeline/selectors.test.ts b/packages/suite-base/src/components/MessagePipeline/selectors.test.ts new file mode 100644 index 0000000000..c7c7fb468f --- /dev/null +++ b/packages/suite-base/src/components/MessagePipeline/selectors.test.ts @@ -0,0 +1,43 @@ +// 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 { getTopicToSchemaNameMap } from "@lichtblick/suite-base/components/MessagePipeline/selectors"; +import { MessagePipelineContext } from "@lichtblick/suite-base/components/MessagePipeline/types"; +import { PlayerPresence } from "@lichtblick/suite-base/players/types"; + +it("map schema names by topic name", () => { + const state: MessagePipelineContext = { + sortedTopics: [ + { name: "topic1", schemaName: "schema1" }, + { name: "topic2", schemaName: "schema2" }, + ], + playerState: { + presence: PlayerPresence.PRESENT, + progress: {}, + capabilities: [], + profile: undefined, + playerId: "", + }, + callService: jest.fn(), + datatypes: new Map(), + fetchAsset: jest.fn(), + messageEventsBySubscriberId: new Map(), + pauseFrame: jest.fn(), + publish: jest.fn(), + seekPlayback: jest.fn(), + setParameter: jest.fn(), + setPublishers: jest.fn(), + setSubscriptions: jest.fn(), + subscriptions: [], + getMetadata: jest.fn(), + }; + const result = getTopicToSchemaNameMap(state); + expect(result).toEqual({ + topic1: "schema1", + topic2: "schema2", + }); +}); diff --git a/packages/suite-base/src/components/MessagePipeline/selectors.ts b/packages/suite-base/src/components/MessagePipeline/selectors.ts new file mode 100644 index 0000000000..ca031e4baa --- /dev/null +++ b/packages/suite-base/src/components/MessagePipeline/selectors.ts @@ -0,0 +1,15 @@ +// 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 * as _ from "lodash-es"; + +import { MessagePipelineContext } from "@lichtblick/suite-base/components/MessagePipeline/types"; + +export const getTopicToSchemaNameMap = ( + state: MessagePipelineContext, +): Record => + _.mapValues(_.keyBy(state.sortedTopics, "name"), ({ schemaName }) => schemaName); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx index c4787f5a68..8ee512b904 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx +++ b/packages/suite-base/src/components/PanelExtensionAdapter/PanelExtensionAdapter.tsx @@ -57,12 +57,12 @@ import { } from "@lichtblick/suite-base/providers/PanelStateContextProvider"; import { PanelConfig, SaveConfig } from "@lichtblick/suite-base/types/panels"; import { assertNever } from "@lichtblick/suite-base/util/assertNever"; +import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; import { PanelConfigVersionError } from "./PanelConfigVersionError"; import { RenderStateConfig, initRenderStateBuilder } from "./renderState"; import { BuiltinPanelExtensionContext } from "./types"; import { useSharedPanelState } from "./useSharedPanelState"; -import { maybeCast } from "@lichtblick/suite-base/util/maybeCast"; const log = Logger.getLogger(__filename); diff --git a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts index 43e36751b5..82e7257482 100644 --- a/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts +++ b/packages/suite-base/src/components/PanelExtensionAdapter/renderState.test.ts @@ -7,6 +7,7 @@ import { produce } from "immer"; +import { PanelSettings } from "@lichtblick/suite"; import { PlayerPresence } from "@lichtblick/suite-base/players/types"; import { BuilderRenderStateInput, initRenderStateBuilder } from "./renderState"; @@ -957,7 +958,7 @@ describe("renderState", () => { const state2 = buildRenderState( produce(initialState, (draft) => { draft.currentFrame = undefined; - draft.playerState.progress.messageCache = undefined; + draft.playerState!.progress.messageCache = undefined; draft.subscriptions.push({ topic: "test", convertTo: "anotherSchema", preload: true }); }), ); diff --git a/packages/suite-base/src/components/PanelSettings/index.tsx b/packages/suite-base/src/components/PanelSettings/index.tsx index 6d9447f739..db4dc2a7b6 100644 --- a/packages/suite-base/src/components/PanelSettings/index.tsx +++ b/packages/suite-base/src/components/PanelSettings/index.tsx @@ -15,10 +15,8 @@ import { SettingsTree } from "@lichtblick/suite"; import { AppSetting } from "@lichtblick/suite-base/AppSetting"; import { useConfigById } from "@lichtblick/suite-base/PanelAPI"; import EmptyState from "@lichtblick/suite-base/components/EmptyState"; -import { - getTopicToSchemaNameMap, - useMessagePipeline, -} from "@lichtblick/suite-base/components/MessagePipeline"; +import { useMessagePipeline } from "@lichtblick/suite-base/components/MessagePipeline"; +import { getTopicToSchemaNameMap } from "@lichtblick/suite-base/components/MessagePipeline/selectors"; import { ActionMenu } from "@lichtblick/suite-base/components/PanelSettings/ActionMenu"; import SettingsTreeEditor from "@lichtblick/suite-base/components/SettingsTreeEditor"; import { ShareJsonModal } from "@lichtblick/suite-base/components/ShareJsonModal";