From 81decc88ec0ee165b62a58fe3026fff6f4f434c2 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Mon, 9 Oct 2023 20:01:56 +0200 Subject: [PATCH 1/9] First implementation --- frontend/src/framework/ModuleContext.ts | 21 +++++++- frontend/src/framework/ModuleInstance.ts | 48 +++++++++++++++++++ .../ViewWrapper/private-components/header.tsx | 30 +++++++++++- .../src/modules/DistributionPlot/view.tsx | 2 + 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index ef519a82d..75b99becf 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -1,10 +1,17 @@ import React from "react"; import { BroadcastChannel } from "./Broadcaster"; -import { ModuleInstance } from "./ModuleInstance"; +import { ModuleInstance, ModuleInstanceLogEntryType } from "./ModuleInstance"; import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; +export enum ModuleInstanceState { + LOADING, + READY, + ERROR, + WARNING, +} + export class ModuleContext { private _moduleInstance: ModuleInstance; private _stateStore: StateStore; @@ -56,4 +63,16 @@ export class ModuleContext { setInstanceTitle(title: string): void { this._moduleInstance.setTitle(title); } + + setLoading(isLoading: boolean): void { + this._moduleInstance.setLoading(isLoading); + } + + log(message: string, type: ModuleInstanceLogEntryType = ModuleInstanceLogEntryType.INFO): void { + this._moduleInstance.log(message, type); + } + + clearLog(): void { + this._moduleInstance.clearLog(); + } } diff --git a/frontend/src/framework/ModuleInstance.ts b/frontend/src/framework/ModuleInstance.ts index 79a154263..97d68e905 100644 --- a/frontend/src/framework/ModuleInstance.ts +++ b/frontend/src/framework/ModuleInstance.ts @@ -17,6 +17,18 @@ export enum ModuleInstanceState { RESETTING, } +export enum ModuleInstanceLogEntryType { + INFO, + WARNING, + ERROR, +} + +export type ModuleInstanceLogEntry = { + message: string; + timestamp: number; + type: ModuleInstanceLogEntryType; +}; + export class ModuleInstance { private _id: string; private _title: string; @@ -35,6 +47,8 @@ export class ModuleInstance { private _cachedDefaultState: StateType | null; private _cachedStateStoreOptions?: StateOptions; private _initialSettings: InitialSettings | null; + private _contentIsLoading: boolean; + private _logEntries: ModuleInstanceLogEntry[]; constructor( module: Module, @@ -57,6 +71,8 @@ export class ModuleInstance { this._fatalError = null; this._cachedDefaultState = null; this._initialSettings = null; + this._contentIsLoading = false; + this._logEntries = []; this._broadcastChannels = {} as Record; @@ -161,6 +177,38 @@ export class ModuleInstance { this.notifySubscribersAboutTitleChange(); } + setLoading(isLoading: boolean): void { + this._contentIsLoading = isLoading; + } + + isLoading(): boolean { + return this._contentIsLoading; + } + + log(message: string, type: ModuleInstanceLogEntryType = ModuleInstanceLogEntryType.INFO): void { + this._logEntries.push({ + message, + timestamp: Date.now(), + type, + }); + } + + getLogEntries(): ModuleInstanceLogEntry[] { + return this._logEntries; + } + + clearLog(): void { + this._logEntries = []; + } + + hasLoggedErrors(): boolean { + return this._logEntries.some((entry) => entry.type === ModuleInstanceLogEntryType.ERROR); + } + + hasLoggedWarnings(): boolean { + return this._logEntries.some((entry) => entry.type === ModuleInstanceLogEntryType.WARNING); + } + subscribeToTitleChange(cb: (title: string) => void): () => void { this._titleChangeSubscribers.add(cb); return () => { diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx index eb0f42009..7093038a3 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx @@ -2,8 +2,9 @@ import React from "react"; import { ModuleInstance } from "@framework/ModuleInstance"; import { SyncSettingKey, SyncSettingsMeta } from "@framework/SyncSettings"; +import { CircularProgress } from "@lib/components/CircularProgress"; import { isDevMode } from "@lib/utils/devMode"; -import { Close } from "@mui/icons-material"; +import { Close, Error, Warning } from "@mui/icons-material"; export type HeaderProps = { moduleInstance: ModuleInstance; @@ -42,6 +43,32 @@ export const Header: React.FC = (props) => { e.stopPropagation(); } + function makeStateIndicator() { + if (props.moduleInstance.isLoading()) { + return ( +
+ +
+ ); + } + + if (props.moduleInstance.hasLoggedErrors()) { + return ( +
+ +
+ ); + } + + if (props.moduleInstance.hasLoggedWarnings()) { + return ( +
+ +
+ ); + } + } + return (
= (props) => { onPointerDown={props.onPointerDown} >
+ {makeStateIndicator()} {title} diff --git a/frontend/src/modules/DistributionPlot/view.tsx b/frontend/src/modules/DistributionPlot/view.tsx index aaf1f8f8d..6c1edf2bf 100644 --- a/frontend/src/modules/DistributionPlot/view.tsx +++ b/frontend/src/modules/DistributionPlot/view.tsx @@ -40,6 +40,8 @@ export const view = ({ moduleContext, workbenchServices, workbenchSettings }: Mo const numBins = moduleContext.useStoreValue("numBins"); const orientation = moduleContext.useStoreValue("orientation"); + moduleContext.setLoading(true); + const [highlightedKey, setHighlightedKey] = React.useState(null); const [dataX, setDataX] = React.useState(null); const [dataY, setDataY] = React.useState(null); From ae309894f63256ca06019281cdd9c6a884769cfe Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Tue, 10 Oct 2023 18:05:09 +0200 Subject: [PATCH 2/9] Improved/refactored implementation --- frontend/src/framework/ModuleContext.ts | 15 +- frontend/src/framework/ModuleInstance.ts | 55 +----- .../ModuleInstanceStatusController.ts | 34 ++++ frontend/src/framework/StatusWriter.ts | 26 +++ .../ModuleInstanceStatusControllerPrivate.ts | 61 ++++++ .../ViewWrapper/private-components/header.tsx | 183 ++++++++++++++---- .../src/modules/DistributionPlot/view.tsx | 2 - frontend/src/modules/MyModule/view.tsx | 20 ++ .../unit-tests/EnsembleParameters.test.ts | 2 - 9 files changed, 297 insertions(+), 101 deletions(-) create mode 100644 frontend/src/framework/ModuleInstanceStatusController.ts create mode 100644 frontend/src/framework/StatusWriter.ts create mode 100644 frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index 75b99becf..7448177c6 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -1,7 +1,8 @@ import React from "react"; import { BroadcastChannel } from "./Broadcaster"; -import { ModuleInstance, ModuleInstanceLogEntryType } from "./ModuleInstance"; +import { ModuleInstance } from "./ModuleInstance"; +import { ModuleInstanceStatusController } from "./ModuleInstanceStatusController"; import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; @@ -64,15 +65,7 @@ export class ModuleContext { this._moduleInstance.setTitle(title); } - setLoading(isLoading: boolean): void { - this._moduleInstance.setLoading(isLoading); - } - - log(message: string, type: ModuleInstanceLogEntryType = ModuleInstanceLogEntryType.INFO): void { - this._moduleInstance.log(message, type); - } - - clearLog(): void { - this._moduleInstance.clearLog(); + getModuleInstanceStatusController(): ModuleInstanceStatusController { + return this._moduleInstance.getStatusController(); } } diff --git a/frontend/src/framework/ModuleInstance.ts b/frontend/src/framework/ModuleInstance.ts index 97d68e905..c53454484 100644 --- a/frontend/src/framework/ModuleInstance.ts +++ b/frontend/src/framework/ModuleInstance.ts @@ -9,6 +9,7 @@ import { ModuleContext } from "./ModuleContext"; import { StateBaseType, StateOptions, StateStore } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; import { Workbench } from "./Workbench"; +import { ModuleInstanceStatusControllerPrivate } from "./internal/ModuleInstanceStatusControllerPrivate"; export enum ModuleInstanceState { INITIALIZING, @@ -17,18 +18,6 @@ export enum ModuleInstanceState { RESETTING, } -export enum ModuleInstanceLogEntryType { - INFO, - WARNING, - ERROR, -} - -export type ModuleInstanceLogEntry = { - message: string; - timestamp: number; - type: ModuleInstanceLogEntryType; -}; - export class ModuleInstance { private _id: string; private _title: string; @@ -47,8 +36,7 @@ export class ModuleInstance { private _cachedDefaultState: StateType | null; private _cachedStateStoreOptions?: StateOptions; private _initialSettings: InitialSettings | null; - private _contentIsLoading: boolean; - private _logEntries: ModuleInstanceLogEntry[]; + private _statusController: ModuleInstanceStatusControllerPrivate; constructor( module: Module, @@ -71,8 +59,7 @@ export class ModuleInstance { this._fatalError = null; this._cachedDefaultState = null; this._initialSettings = null; - this._contentIsLoading = false; - this._logEntries = []; + this._statusController = new ModuleInstanceStatusControllerPrivate(); this._broadcastChannels = {} as Record; @@ -177,38 +164,6 @@ export class ModuleInstance { this.notifySubscribersAboutTitleChange(); } - setLoading(isLoading: boolean): void { - this._contentIsLoading = isLoading; - } - - isLoading(): boolean { - return this._contentIsLoading; - } - - log(message: string, type: ModuleInstanceLogEntryType = ModuleInstanceLogEntryType.INFO): void { - this._logEntries.push({ - message, - timestamp: Date.now(), - type, - }); - } - - getLogEntries(): ModuleInstanceLogEntry[] { - return this._logEntries; - } - - clearLog(): void { - this._logEntries = []; - } - - hasLoggedErrors(): boolean { - return this._logEntries.some((entry) => entry.type === ModuleInstanceLogEntryType.ERROR); - } - - hasLoggedWarnings(): boolean { - return this._logEntries.some((entry) => entry.type === ModuleInstanceLogEntryType.WARNING); - } - subscribeToTitleChange(cb: (title: string) => void): () => void { this._titleChangeSubscribers.add(cb); return () => { @@ -226,6 +181,10 @@ export class ModuleInstance { return this._module; } + getStatusController(): ModuleInstanceStatusControllerPrivate { + return this._statusController; + } + subscribeToImportStateChange(cb: () => void): () => void { this._importStateSubscribers.add(cb); return () => { diff --git a/frontend/src/framework/ModuleInstanceStatusController.ts b/frontend/src/framework/ModuleInstanceStatusController.ts new file mode 100644 index 000000000..0b191b1fa --- /dev/null +++ b/frontend/src/framework/ModuleInstanceStatusController.ts @@ -0,0 +1,34 @@ +export enum ModuleInstanceStatusControllerLogEntryType { + Warning = "warning", + Error = "error", +} + +export type ModuleInstanceStatusControllerLogEntry = { + message: string; + type: ModuleInstanceStatusControllerLogEntryType; +}; + +export class ModuleInstanceStatusController { + protected _logEntries: ModuleInstanceStatusControllerLogEntry[]; + protected _isLoading: boolean; + + constructor() { + this._logEntries = []; + this._isLoading = false; + } + + logMessage(message: string, type: ModuleInstanceStatusControllerLogEntryType): void { + this._logEntries.push({ + message, + type, + }); + } + + clearLog(): void { + this._logEntries = []; + } + + setLoading(isLoading: boolean): void { + this._isLoading = isLoading; + } +} diff --git a/frontend/src/framework/StatusWriter.ts b/frontend/src/framework/StatusWriter.ts new file mode 100644 index 000000000..abad6d2dd --- /dev/null +++ b/frontend/src/framework/StatusWriter.ts @@ -0,0 +1,26 @@ +import { ModuleContext } from "./ModuleContext"; +import { + ModuleInstanceStatusController, + ModuleInstanceStatusControllerLogEntryType, +} from "./ModuleInstanceStatusController"; + +export class StatusWriter { + private _statusController: ModuleInstanceStatusController; + + constructor(moduleContext: ModuleContext) { + this._statusController = moduleContext.getModuleInstanceStatusController(); + this._statusController.clearLog(); + } + + setLoading(isLoading: boolean): void { + this._statusController.setLoading(isLoading); + } + + addError(message: string): void { + this._statusController.logMessage(message, ModuleInstanceStatusControllerLogEntryType.Error); + } + + addWarning(message: string): void { + this._statusController.logMessage(message, ModuleInstanceStatusControllerLogEntryType.Warning); + } +} diff --git a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts new file mode 100644 index 000000000..d2b5f33ad --- /dev/null +++ b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts @@ -0,0 +1,61 @@ +import { + ModuleInstanceStatusController, + ModuleInstanceStatusControllerLogEntry, + ModuleInstanceStatusControllerLogEntryType, +} from "@framework/ModuleInstanceStatusController"; + +export enum ModuleInstaceStatusControllerTopics { + LogChange = "log-change", + LoadingStateChange = "loading-state-change", +} + +export class ModuleInstanceStatusControllerPrivate extends ModuleInstanceStatusController { + private _subscribers: Map void>>; + + constructor() { + super(); + this._subscribers = new Map(); + } + + logMessage(message: string, type: ModuleInstanceStatusControllerLogEntryType): void { + super.logMessage(message, type); + this.notifySubscribers(ModuleInstaceStatusControllerTopics.LogChange); + } + + getLogEntries(): readonly ModuleInstanceStatusControllerLogEntry[] { + return this._logEntries; + } + + clearLog(): void { + super.clearLog(); + this.notifySubscribers(ModuleInstaceStatusControllerTopics.LogChange); + } + + setLoading(isLoading: boolean): void { + super.setLoading(isLoading); + this.notifySubscribers(ModuleInstaceStatusControllerTopics.LoadingStateChange); + } + + isLoading(): boolean { + return this._isLoading; + } + + subscribeToTopic(topic: ModuleInstaceStatusControllerTopics, cb: () => void): () => void { + const subscribers = this._subscribers.get(topic) || new Set(); + subscribers.add(cb); + this._subscribers.set(topic, subscribers); + + return () => { + subscribers.delete(cb); + }; + } + + private notifySubscribers(topic: ModuleInstaceStatusControllerTopics): void { + const subscribers = this._subscribers.get(topic); + if (subscribers) { + subscribers.forEach((subscriber) => { + subscriber(); + }); + } + } +} diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx index 7093038a3..45676ddc6 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx @@ -1,9 +1,18 @@ import React from "react"; +import ReactDOM from "react-dom"; import { ModuleInstance } from "@framework/ModuleInstance"; +import { + ModuleInstanceStatusControllerLogEntry, + ModuleInstanceStatusControllerLogEntryType, +} from "@framework/ModuleInstanceStatusController"; import { SyncSettingKey, SyncSettingsMeta } from "@framework/SyncSettings"; +import { ModuleInstaceStatusControllerTopics } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; +import { Badge } from "@lib/components/Badge"; import { CircularProgress } from "@lib/components/CircularProgress"; +import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { isDevMode } from "@lib/utils/devMode"; +import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { Close, Error, Warning } from "@mui/icons-material"; export type HeaderProps = { @@ -18,66 +27,155 @@ export const Header: React.FC = (props) => { props.moduleInstance.getSyncedSettingKeys() ); const [title, setTitle] = React.useState(props.moduleInstance.getTitle()); + const [isLoading, setIsLoading] = React.useState(props.moduleInstance.getStatusController().isLoading()); + const [logEntries, setLogEntries] = React.useState([]); + const [logVisible, setLogVisible] = React.useState(false); - React.useEffect(() => { + const ref = React.useRef(null); + const boundingRect = useElementBoundingRect(ref); + + React.useEffect(function handleMount() { function handleSyncedSettingsChange(newSyncedSettings: SyncSettingKey[]) { setSyncedSettings([...newSyncedSettings]); } - const unsubscribeFunc = props.moduleInstance.subscribeToSyncedSettingKeysChange(handleSyncedSettingsChange); - - return unsubscribeFunc; - }, []); - - React.useEffect(() => { function handleTitleChange(newTitle: string) { setTitle(newTitle); } - const unsubscribeFunc = props.moduleInstance.subscribeToTitleChange(handleTitleChange); + function handleLoadingChange() { + setIsLoading(props.moduleInstance.getStatusController().isLoading()); + } + + function handleLogEntriesChanges() { + setLogEntries([...props.moduleInstance.getStatusController().getLogEntries()]); + } + + const unsubscribeFromSyncSettingsChange = + props.moduleInstance.subscribeToSyncedSettingKeysChange(handleSyncedSettingsChange); + const unsubscribeFromTitleChange = props.moduleInstance.subscribeToTitleChange(handleTitleChange); + const unsubscribeFromLoadingChange = props.moduleInstance + .getStatusController() + .subscribeToTopic(ModuleInstaceStatusControllerTopics.LoadingStateChange, handleLoadingChange); + const unsubscribeFromLogEntriesChange = props.moduleInstance + .getStatusController() + .subscribeToTopic(ModuleInstaceStatusControllerTopics.LogChange, handleLogEntriesChanges); - return unsubscribeFunc; + return function handleUnmount() { + unsubscribeFromSyncSettingsChange(); + unsubscribeFromTitleChange(); + unsubscribeFromLoadingChange(); + unsubscribeFromLogEntriesChange(); + }; }, []); + function handlePointerDown(e: React.PointerEvent) { + props.onPointerDown(e); + setLogVisible(false); + } + function handlePointerUp(e: React.PointerEvent) { e.stopPropagation(); } - function makeStateIndicator() { - if (props.moduleInstance.isLoading()) { - return ( -
- -
- ); - } + function handleStatusPointerDown(e: React.PointerEvent) { + setLogVisible(!logVisible); + e.stopPropagation(); + } - if (props.moduleInstance.hasLoggedErrors()) { - return ( -
- + function makeStatusIndicator(): React.ReactNode { + const stateIndicators: React.ReactNode[] = []; + + if (isLoading) { + stateIndicators.push( +
+
); } + const numErrors = logEntries.filter( + (entry) => entry.type === ModuleInstanceStatusControllerLogEntryType.Error + ).length; + const numWarnings = logEntries.filter( + (entry) => entry.type === ModuleInstanceStatusControllerLogEntryType.Warning + ).length; - if (props.moduleInstance.hasLoggedWarnings()) { - return ( -
- + if (numErrors > 0 || numWarnings > 0) { + stateIndicators.push( +
+ + +
+ 0, + })} + /> +
+
); } + + if (stateIndicators.length === 0) return null; + + return ( +
+ {stateIndicators} + +
+ ); + } + + function makeLogEntries(): React.ReactNode { + return ( +
+ {logEntries.map((entry, i) => ( +
+ {entry.type === ModuleInstanceStatusControllerLogEntryType.Error && ( + + )} + {entry.type === ModuleInstanceStatusControllerLogEntryType.Warning && ( + + )} + + {entry.message} + +
+ ))} +
+ ); } return (
-
- {makeStateIndicator()} + {makeStatusIndicator()} +
{title} @@ -100,16 +198,25 @@ export const Header: React.FC = (props) => { ))} +
+ +
- -
- -
+ {logVisible && + ReactDOM.createPortal( +
+ {makeLogEntries()} +
, + document.body + )}
); }; diff --git a/frontend/src/modules/DistributionPlot/view.tsx b/frontend/src/modules/DistributionPlot/view.tsx index 6c1edf2bf..aaf1f8f8d 100644 --- a/frontend/src/modules/DistributionPlot/view.tsx +++ b/frontend/src/modules/DistributionPlot/view.tsx @@ -40,8 +40,6 @@ export const view = ({ moduleContext, workbenchServices, workbenchSettings }: Mo const numBins = moduleContext.useStoreValue("numBins"); const orientation = moduleContext.useStoreValue("orientation"); - moduleContext.setLoading(true); - const [highlightedKey, setHighlightedKey] = React.useState(null); const [dataX, setDataX] = React.useState(null); const [dataY, setDataY] = React.useState(null); diff --git a/frontend/src/modules/MyModule/view.tsx b/frontend/src/modules/MyModule/view.tsx index 21d99a6ad..c2e4f1155 100644 --- a/frontend/src/modules/MyModule/view.tsx +++ b/frontend/src/modules/MyModule/view.tsx @@ -2,6 +2,8 @@ import React from "react"; import Plot from "react-plotly.js"; import { ModuleFCProps } from "@framework/Module"; +import { ModuleInstanceStatusControllerLogEntryType } from "@framework/ModuleInstanceStatusController"; +import { StatusWriter } from "@framework/StatusWriter"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ColorScaleType } from "@lib/utils/ColorScale"; @@ -411,6 +413,24 @@ export const view = (props: ModuleFCProps) => { const ref = React.useRef(null); + const statusWriter = new StatusWriter(props.moduleContext); + + statusWriter.setLoading(true); + + if (type === ColorScaleType.Continuous) { + statusWriter.addWarning("Continuous selected"); + } + + if (gradientType === "diverging") { + statusWriter.addWarning("Diverging selected"); + } + + React.useEffect(() => { + setTimeout(() => { + statusWriter.setLoading(false); + }, 5000); + }, [type, gradientType]); + const size = useElementSize(ref); const colorScale = diff --git a/frontend/tests/unit-tests/EnsembleParameters.test.ts b/frontend/tests/unit-tests/EnsembleParameters.test.ts index a9f22d2fe..53e7e7572 100644 --- a/frontend/tests/unit-tests/EnsembleParameters.test.ts +++ b/frontend/tests/unit-tests/EnsembleParameters.test.ts @@ -12,7 +12,6 @@ const PARAM_ARR: Parameter[] = [ {type: ParameterType.DISCRETE, name: "dparam_B", groupName: null, description: "descB", isConstant: false, realizations: [1,2,3], values: ["A", "B", "C"]}, ]; - describe("EnsembleParameters tests", () => { test("Get list of parameter idents", () => { const ensParams = new EnsembleParameters(PARAM_ARR); @@ -98,7 +97,6 @@ describe("EnsembleParameters tests", () => { }); }); - describe("ParameterIdent tests", () => { test("Conversion to/from string", () => { { From 2c711783b0145b19847c88a2475de2f32c742bc1 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 12 Oct 2023 11:54:54 +0200 Subject: [PATCH 3/9] Final adjustments to make it work with React workflow --- frontend/src/framework/ModuleContext.ts | 4 + .../ModuleInstanceStatusController.ts | 77 +++++++++++-- frontend/src/framework/StatusWriter.ts | 26 ----- frontend/src/framework/StatusWriter.tsx | 77 +++++++++++++ .../ModuleInstanceStatusControllerPrivate.ts | 89 +++++++++------ .../ViewWrapper/private-components/header.tsx | 106 ++++++++---------- .../private-components/viewContent.tsx | 46 ++++---- .../DebugProfiler/debugProfiler.tsx | 29 ++++- .../Settings/private-components/setting.tsx | 7 +- .../private-components/tag.tsx | 2 - frontend/src/modules/MyModule/view.tsx | 18 +-- .../SimulationTimeSeriesMatrix/view.tsx | 17 ++- .../components/MessageContent/index.ts | 1 + .../MessageContent/messageContent.tsx | 47 ++++++++ frontend/tailwind.config.cjs | 15 ++- 15 files changed, 391 insertions(+), 170 deletions(-) delete mode 100644 frontend/src/framework/StatusWriter.ts create mode 100644 frontend/src/framework/StatusWriter.tsx create mode 100644 frontend/src/modules/_shared/components/MessageContent/index.ts create mode 100644 frontend/src/modules/_shared/components/MessageContent/messageContent.tsx diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index 7448177c6..fa45a4d13 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -68,4 +68,8 @@ export class ModuleContext { getModuleInstanceStatusController(): ModuleInstanceStatusController { return this._moduleInstance.getStatusController(); } + + getSource(): string { + throw Error("Not implemented"); + } } diff --git a/frontend/src/framework/ModuleInstanceStatusController.ts b/frontend/src/framework/ModuleInstanceStatusController.ts index 0b191b1fa..65ad8de96 100644 --- a/frontend/src/framework/ModuleInstanceStatusController.ts +++ b/frontend/src/framework/ModuleInstanceStatusController.ts @@ -1,34 +1,87 @@ -export enum ModuleInstanceStatusControllerLogEntryType { +import { cloneDeep } from "lodash"; + +export enum StatusMessageType { Warning = "warning", Error = "error", } -export type ModuleInstanceStatusControllerLogEntry = { +export enum StatusSource { + View = "view", + Settings = "settings", +} + +export type StatusMessage = { + source: StatusSource; message: string; - type: ModuleInstanceStatusControllerLogEntryType; + type: StatusMessageType; +}; + +export type StatusControllerState = { + messages: StatusMessage[]; + loading: boolean; + viewDebugMessage: string; + settingsDebugMessage: string; + viewRenderCount: number | null; + settingsRenderCount: number | null; }; export class ModuleInstanceStatusController { - protected _logEntries: ModuleInstanceStatusControllerLogEntry[]; - protected _isLoading: boolean; + protected _stateCandidates: StatusControllerState; + protected _state: StatusControllerState; constructor() { - this._logEntries = []; - this._isLoading = false; + this._state = { + messages: [], + loading: false, + viewDebugMessage: "", + settingsDebugMessage: "", + viewRenderCount: null, + settingsRenderCount: null, + }; + this._stateCandidates = cloneDeep(this._state); } - logMessage(message: string, type: ModuleInstanceStatusControllerLogEntryType): void { - this._logEntries.push({ + addMessage(source: StatusSource, message: string, type: StatusMessageType): void { + this._stateCandidates.messages.push({ + source, message, type, }); } - clearLog(): void { - this._logEntries = []; + clearMessages(source: StatusSource): void { + this._stateCandidates.messages = this._stateCandidates.messages.filter((msg) => msg.source !== source); } setLoading(isLoading: boolean): void { - this._isLoading = isLoading; + this._stateCandidates.loading = isLoading; + } + + setDebugMessage(source: StatusSource, message: string): void { + if (source === StatusSource.View) { + this._stateCandidates.viewDebugMessage = message; + } + if (source === StatusSource.Settings) { + this._stateCandidates.settingsDebugMessage = message; + } + } + + incrementReportedComponentRenderCount(source: StatusSource): void { + if (source === StatusSource.View) { + if (this._stateCandidates.viewRenderCount === null) { + this._stateCandidates.viewRenderCount = 0; + } + this._stateCandidates.viewRenderCount++; + } + if (source === StatusSource.Settings) { + if (this._stateCandidates.settingsRenderCount === null) { + this._stateCandidates.settingsRenderCount = 0; + } + this._stateCandidates.settingsRenderCount++; + } + } + + reviseState(): void { + this._state = cloneDeep(this._stateCandidates); } } diff --git a/frontend/src/framework/StatusWriter.ts b/frontend/src/framework/StatusWriter.ts deleted file mode 100644 index abad6d2dd..000000000 --- a/frontend/src/framework/StatusWriter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ModuleContext } from "./ModuleContext"; -import { - ModuleInstanceStatusController, - ModuleInstanceStatusControllerLogEntryType, -} from "./ModuleInstanceStatusController"; - -export class StatusWriter { - private _statusController: ModuleInstanceStatusController; - - constructor(moduleContext: ModuleContext) { - this._statusController = moduleContext.getModuleInstanceStatusController(); - this._statusController.clearLog(); - } - - setLoading(isLoading: boolean): void { - this._statusController.setLoading(isLoading); - } - - addError(message: string): void { - this._statusController.logMessage(message, ModuleInstanceStatusControllerLogEntryType.Error); - } - - addWarning(message: string): void { - this._statusController.logMessage(message, ModuleInstanceStatusControllerLogEntryType.Warning); - } -} diff --git a/frontend/src/framework/StatusWriter.tsx b/frontend/src/framework/StatusWriter.tsx new file mode 100644 index 000000000..3bb0b4262 --- /dev/null +++ b/frontend/src/framework/StatusWriter.tsx @@ -0,0 +1,77 @@ +import React from "react"; + +import { ModuleContext } from "./ModuleContext"; +import { ModuleInstanceStatusController, StatusMessageType, StatusSource } from "./ModuleInstanceStatusController"; + +export class ViewStatusWriter { + private _statusController: ModuleInstanceStatusController; + + constructor(moduleContext: ModuleContext) { + this._statusController = moduleContext.getModuleInstanceStatusController(); + } + + setLoading(isLoading: boolean): void { + this._statusController.setLoading(isLoading); + } + + addError(message: string): void { + this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Error); + } + + addWarning(message: string): void { + this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Warning); + } + + setDebugMessage(message: string): void { + this._statusController.setDebugMessage(StatusSource.View, message); + } +} + +export class SettingsStatusWriter { + private _statusController: ModuleInstanceStatusController; + + constructor(moduleContext: ModuleContext) { + this._statusController = moduleContext.getModuleInstanceStatusController(); + } + + addError(message: string): void { + this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Error); + } + + addWarning(message: string): void { + this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Warning); + } + + setDebugMessage(message: string): void { + this._statusController.setDebugMessage(StatusSource.Settings, message); + } +} + +function useStatusWriter< + T extends StatusSource, + TStatusWriter = T extends StatusSource.View ? ViewStatusWriter : SettingsStatusWriter +>(moduleContext: ModuleContext, statusSource: T): TStatusWriter { + const statusWriter = React.useRef( + (statusSource === StatusSource.View + ? new ViewStatusWriter(moduleContext) + : new SettingsStatusWriter(moduleContext)) as TStatusWriter + ); + + const statusController = moduleContext.getModuleInstanceStatusController(); + statusController.clearMessages(statusSource); + statusController.incrementReportedComponentRenderCount(statusSource); + + React.useEffect(function handleRender() { + statusController.reviseState(); + }); + + return statusWriter.current; +} + +export function useViewStatusWriter(moduleContext: ModuleContext): ViewStatusWriter { + return useStatusWriter(moduleContext, StatusSource.View); +} + +export function useSettingsStatusWriter(moduleContext: ModuleContext): SettingsStatusWriter { + return useStatusWriter(moduleContext, StatusSource.Settings); +} diff --git a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts index d2b5f33ad..7e07bd8e3 100644 --- a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts +++ b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts @@ -1,61 +1,80 @@ +import React from "react"; + import { ModuleInstanceStatusController, - ModuleInstanceStatusControllerLogEntry, - ModuleInstanceStatusControllerLogEntryType, + StatusControllerState, + StatusSource, } from "@framework/ModuleInstanceStatusController"; -export enum ModuleInstaceStatusControllerTopics { - LogChange = "log-change", - LoadingStateChange = "loading-state-change", -} +import { filter, isEqual, keys } from "lodash"; export class ModuleInstanceStatusControllerPrivate extends ModuleInstanceStatusController { - private _subscribers: Map void>>; + private _subscribers: Map void>>; constructor() { super(); this._subscribers = new Map(); } - logMessage(message: string, type: ModuleInstanceStatusControllerLogEntryType): void { - super.logMessage(message, type); - this.notifySubscribers(ModuleInstaceStatusControllerTopics.LogChange); + clearMessages(source: StatusSource): void { + super.clearMessages(source); } - getLogEntries(): readonly ModuleInstanceStatusControllerLogEntry[] { - return this._logEntries; - } + reviseState(): void { + const differentStateKeys = filter(keys(this._stateCandidates), (key: keyof StatusControllerState) => { + return !isEqual(this._state[key], this._stateCandidates[key]); + }) as (keyof StatusControllerState)[]; - clearLog(): void { - super.clearLog(); - this.notifySubscribers(ModuleInstaceStatusControllerTopics.LogChange); + super.reviseState(); + + differentStateKeys.forEach((stateKey) => { + this.notifySubscribers(stateKey); + }); } - setLoading(isLoading: boolean): void { - super.setLoading(isLoading); - this.notifySubscribers(ModuleInstaceStatusControllerTopics.LoadingStateChange); + private notifySubscribers(stateKey: keyof StatusControllerState): void { + const subscribers = this._subscribers.get(stateKey); + if (subscribers) { + subscribers.forEach((subscriber) => { + subscriber(); + }); + } } - isLoading(): boolean { - return this._isLoading; + makeSnapshotGetter(stateKey: T): () => StatusControllerState[T] { + const snapshotGetter = (): any => { + return this._state[stateKey]; + }; + + return snapshotGetter; } - subscribeToTopic(topic: ModuleInstaceStatusControllerTopics, cb: () => void): () => void { - const subscribers = this._subscribers.get(topic) || new Set(); - subscribers.add(cb); - this._subscribers.set(topic, subscribers); + makeSubscriberFunction( + stateKey: T + ): (onStoreChangeCallback: () => void) => () => void { + // Using arrow function in order to keep "this" in context + const subscriber = (onStoreChangeCallback: () => void): (() => void) => { + const subscribers = this._subscribers.get(stateKey) || new Set(); + subscribers.add(onStoreChangeCallback); + this._subscribers.set(stateKey, subscribers); - return () => { - subscribers.delete(cb); + return () => { + subscribers.delete(onStoreChangeCallback); + }; }; - } - private notifySubscribers(topic: ModuleInstaceStatusControllerTopics): void { - const subscribers = this._subscribers.get(topic); - if (subscribers) { - subscribers.forEach((subscriber) => { - subscriber(); - }); - } + return subscriber; } } + +export function useStatusControllerValue( + statusController: ModuleInstanceStatusControllerPrivate, + stateKey: T +): StatusControllerState[T] { + const value = React.useSyncExternalStore( + statusController.makeSubscriberFunction(stateKey), + statusController.makeSnapshotGetter(stateKey) + ); + + return value; +} diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx index 45676ddc6..77e1aab27 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx @@ -2,12 +2,9 @@ import React from "react"; import ReactDOM from "react-dom"; import { ModuleInstance } from "@framework/ModuleInstance"; -import { - ModuleInstanceStatusControllerLogEntry, - ModuleInstanceStatusControllerLogEntryType, -} from "@framework/ModuleInstanceStatusController"; +import { StatusMessageType } from "@framework/ModuleInstanceStatusController"; import { SyncSettingKey, SyncSettingsMeta } from "@framework/SyncSettings"; -import { ModuleInstaceStatusControllerTopics } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; +import { useStatusControllerValue } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; import { Badge } from "@lib/components/Badge"; import { CircularProgress } from "@lib/components/CircularProgress"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; @@ -27,9 +24,9 @@ export const Header: React.FC = (props) => { props.moduleInstance.getSyncedSettingKeys() ); const [title, setTitle] = React.useState(props.moduleInstance.getTitle()); - const [isLoading, setIsLoading] = React.useState(props.moduleInstance.getStatusController().isLoading()); - const [logEntries, setLogEntries] = React.useState([]); - const [logVisible, setLogVisible] = React.useState(false); + const isLoading = useStatusControllerValue(props.moduleInstance.getStatusController(), "loading"); + const statusMessages = useStatusControllerValue(props.moduleInstance.getStatusController(), "messages"); + const [statusMessagesVisible, setStatusMessagesVisible] = React.useState(false); const ref = React.useRef(null); const boundingRect = useElementBoundingRect(ref); @@ -43,35 +40,19 @@ export const Header: React.FC = (props) => { setTitle(newTitle); } - function handleLoadingChange() { - setIsLoading(props.moduleInstance.getStatusController().isLoading()); - } - - function handleLogEntriesChanges() { - setLogEntries([...props.moduleInstance.getStatusController().getLogEntries()]); - } - const unsubscribeFromSyncSettingsChange = props.moduleInstance.subscribeToSyncedSettingKeysChange(handleSyncedSettingsChange); const unsubscribeFromTitleChange = props.moduleInstance.subscribeToTitleChange(handleTitleChange); - const unsubscribeFromLoadingChange = props.moduleInstance - .getStatusController() - .subscribeToTopic(ModuleInstaceStatusControllerTopics.LoadingStateChange, handleLoadingChange); - const unsubscribeFromLogEntriesChange = props.moduleInstance - .getStatusController() - .subscribeToTopic(ModuleInstaceStatusControllerTopics.LogChange, handleLogEntriesChanges); return function handleUnmount() { unsubscribeFromSyncSettingsChange(); unsubscribeFromTitleChange(); - unsubscribeFromLoadingChange(); - unsubscribeFromLogEntriesChange(); }; }, []); function handlePointerDown(e: React.PointerEvent) { props.onPointerDown(e); - setLogVisible(false); + setStatusMessagesVisible(false); } function handlePointerUp(e: React.PointerEvent) { @@ -79,7 +60,7 @@ export const Header: React.FC = (props) => { } function handleStatusPointerDown(e: React.PointerEvent) { - setLogVisible(!logVisible); + setStatusMessagesVisible(!statusMessagesVisible); e.stopPropagation(); } @@ -89,6 +70,7 @@ export const Header: React.FC = (props) => { if (isLoading) { stateIndicators.push(
@@ -96,19 +78,16 @@ export const Header: React.FC = (props) => {
); } - const numErrors = logEntries.filter( - (entry) => entry.type === ModuleInstanceStatusControllerLogEntryType.Error - ).length; - const numWarnings = logEntries.filter( - (entry) => entry.type === ModuleInstanceStatusControllerLogEntryType.Warning - ).length; + const numErrors = statusMessages.filter((message) => message.type === StatusMessageType.Error).length; + const numWarnings = statusMessages.filter((message) => message.type === StatusMessageType.Warning).length; if (numErrors > 0 || numWarnings > 0) { stateIndicators.push(
@@ -137,23 +116,20 @@ export const Header: React.FC = (props) => { return (
+ {stateIndicators}
); } - function makeLogEntries(): React.ReactNode { + function makeStatusMessages(): React.ReactNode { return (
- {logEntries.map((entry, i) => ( -
- {entry.type === ModuleInstanceStatusControllerLogEntryType.Error && ( - - )} - {entry.type === ModuleInstanceStatusControllerLogEntryType.Warning && ( - - )} + {statusMessages.map((entry, i) => ( +
+ {entry.type === StatusMessageType.Error && } + {entry.type === StatusMessageType.Warning && } = (props) => { ); } + const hasErrors = statusMessages.some((entry) => entry.type === StatusMessageType.Error); + return (
- {makeStatusIndicator()} +
+
+
{title} @@ -198,22 +185,27 @@ export const Header: React.FC = (props) => { ))} -
- -
- {logVisible && + {makeStatusIndicator()} +
+ +
+ {statusMessagesVisible && ReactDOM.createPortal(
- {makeLogEntries()} + {makeStatusMessages()}
, document.body )} diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/viewContent.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/viewContent.tsx index f082fe1cb..2d4e2fce5 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/viewContent.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/viewContent.tsx @@ -2,6 +2,7 @@ import React from "react"; import { ImportState } from "@framework/Module"; import { ModuleInstance, ModuleInstanceState } from "@framework/ModuleInstance"; +import { StatusSource } from "@framework/ModuleInstanceStatusController"; import { Workbench } from "@framework/Workbench"; import { DebugProfiler } from "@framework/internal/components/DebugProfiler"; import { ErrorBoundary } from "@framework/internal/components/ErrorBoundary"; @@ -20,39 +21,40 @@ export const ViewContent = React.memo((props: ViewContentProps) => { ModuleInstanceState.INITIALIZING ); - React.useEffect(() => { + React.useEffect(function handleMount() { + setModuleInstanceState(props.moduleInstance.getModuleInstanceState()); setImportState(props.moduleInstance.getImportState()); function handleModuleInstanceImportStateChange() { setImportState(props.moduleInstance.getImportState()); } - const unsubscribeFunc = props.moduleInstance.subscribeToImportStateChange( - handleModuleInstanceImportStateChange - ); - - return unsubscribeFunc; - }, []); - - React.useEffect(() => { - setModuleInstanceState(props.moduleInstance.getModuleInstanceState()); - function handleModuleInstanceStateChange() { setModuleInstanceState(props.moduleInstance.getModuleInstanceState()); } - const unsubscribeFunc = props.moduleInstance.subscribeToModuleInstanceStateChange( + const unsubscribeFromImportStateChange = props.moduleInstance.subscribeToImportStateChange( + handleModuleInstanceImportStateChange + ); + + const unsubscribeFromModuleInstanceStateChange = props.moduleInstance.subscribeToModuleInstanceStateChange( handleModuleInstanceStateChange ); - return unsubscribeFunc; + return function handleUnmount() { + unsubscribeFromImportStateChange(); + unsubscribeFromModuleInstanceStateChange(); + }; }, []); - const handleModuleInstanceReload = React.useCallback(() => { - props.moduleInstance.reset().then(() => { - setModuleInstanceState(props.moduleInstance.getModuleInstanceState()); - }); - }, [props.moduleInstance]); + const handleModuleInstanceReload = React.useCallback( + function handleModuleInstanceReload() { + props.moduleInstance.reset().then(() => { + setModuleInstanceState(props.moduleInstance.getModuleInstanceState()); + }); + }, + [props.moduleInstance] + ); if (importState === ImportState.NotImported) { return
Not imported
; @@ -114,8 +116,12 @@ export const ViewContent = React.memo((props: ViewContentProps) => { const View = props.moduleInstance.getViewFC(); return ( -
- +
+ = (props) => { const [renderInfo, setRenderInfo] = React.useState(null); + const reportedRenderCount = useStatusControllerValue( + props.statusController, + props.source === StatusSource.View ? "viewRenderCount" : "settingsRenderCount" + ); + const customDebugMessage = useStatusControllerValue( + props.statusController, + props.source === StatusSource.View ? "viewDebugMessage" : "settingsDebugMessage" + ); const handleRender = React.useCallback( ( @@ -91,8 +106,18 @@ export const DebugProfiler: React.FC = (props) => {
{renderInfo && ( <> - - RC: {renderInfo.renderCount} + {reportedRenderCount !== null && ( + + Component RC: {reportedRenderCount} + + )} + {customDebugMessage && ( + + Message: {customDebugMessage} + + )} + + Tree RC: {renderInfo.renderCount} P: {renderInfo.phase} = (props) => {
- + ) => { + console.debug("rendering mymodule"); const type = props.moduleContext.useStoreValue("type"); const gradientType = props.moduleContext.useStoreValue("gradientType"); const min = props.moduleContext.useStoreValue("min"); const max = props.moduleContext.useStoreValue("max"); const divMidPoint = props.moduleContext.useStoreValue("divMidPoint"); - const ref = React.useRef(null); - - const statusWriter = new StatusWriter(props.moduleContext); + const statusWriter = useViewStatusWriter(props.moduleContext); - statusWriter.setLoading(true); + const ref = React.useRef(null); if (type === ColorScaleType.Continuous) { - statusWriter.addWarning("Continuous selected"); + statusWriter.addError("Continuous selected"); } if (gradientType === "diverging") { statusWriter.addWarning("Diverging selected"); } - React.useEffect(() => { - setTimeout(() => { - statusWriter.setLoading(false); - }, 5000); - }, [type, gradientType]); - const size = useElementSize(ref); const colorScale = diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx index 8ef285fa2..433be773b 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx @@ -4,9 +4,11 @@ import Plot from "react-plotly.js"; import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleFCProps } from "@framework/Module"; +import { ViewStatusWriter, useViewStatusWriter } from "@framework/StatusWriter"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ColorScaleGradientType } from "@lib/utils/ColorScale"; +import { ContentError } from "@modules/_shared/components/MessageContent/messageContent"; import { useHistoricalVectorDataQueries, useStatisticalVectorDataQueries, useVectorDataQueries } from "./queryHooks"; import { GroupBy, State, VisualizationMode } from "./state"; @@ -24,6 +26,8 @@ export const view = ({ moduleContext, workbenchSession, workbenchSettings }: Mod const ensembleSet = useEnsembleSet(workbenchSession); + const statusWriter = useViewStatusWriter(moduleContext); + // Store values const vectorSpecifications = moduleContext.useStoreValue("vectorSpecifications"); const groupBy = moduleContext.useStoreValue("groupBy"); @@ -64,13 +68,24 @@ export const view = ({ moduleContext, workbenchSession, workbenchSettings }: Mod vectorSpecificationsWithHistoricalData?.some((vec) => vec.hasHistoricalVector) ?? false ); + const isQueryFetching = [ + ...vectorDataQueries.filter((query) => query.isFetching), + ...vectorStatisticsQueries.filter((query) => query.isFetching), + ...historicalVectorDataQueries.filter((query) => query.isFetching), + ]; + + statusWriter.setLoading(isQueryFetching.length > 0); + + statusWriter.setDebugMessage("bla"); + const hasQueryError = [ ...vectorDataQueries.filter((query) => query.isError), ...vectorStatisticsQueries.filter((query) => query.isError), ...historicalVectorDataQueries.filter((query) => query.isError), ]; if (hasQueryError.length > 0) { - return
One or more query has error state
; + statusWriter.addError("One or more queries have an error state."); + return One or more queries have an error state.; } // Map vector specifications and queries with data diff --git a/frontend/src/modules/_shared/components/MessageContent/index.ts b/frontend/src/modules/_shared/components/MessageContent/index.ts new file mode 100644 index 000000000..97cff6270 --- /dev/null +++ b/frontend/src/modules/_shared/components/MessageContent/index.ts @@ -0,0 +1 @@ +export { ContentInfo, ContentError } from "./messageContent"; diff --git a/frontend/src/modules/_shared/components/MessageContent/messageContent.tsx b/frontend/src/modules/_shared/components/MessageContent/messageContent.tsx new file mode 100644 index 000000000..411fd6c3d --- /dev/null +++ b/frontend/src/modules/_shared/components/MessageContent/messageContent.tsx @@ -0,0 +1,47 @@ +import React from "react"; + +import { resolveClassNames } from "@lib/utils/resolveClassNames"; + +export enum ContentMessageType { + INFO = "info", + ERROR = "error", +} + +export type ContentMessageProps = { + type: ContentMessageType; + children: React.ReactNode; +}; + +export const ContentMessage: React.FC = (props) => { + return ( +
+ {props.children} +
+ ); +}; + +ContentMessage.displayName = "MessageContent"; + +export type ContentErrorProps = { + children: React.ReactNode; +}; + +export const ContentError: React.FC = (props) => { + return {props.children}; +}; + +ContentError.displayName = "ContentError"; + +export type ContentInfoProps = { + children: React.ReactNode; +}; + +export const ContentInfo: React.FC = (props) => { + return {props.children}; +}; + +ContentInfo.displayName = "ContentInfo"; diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs index a1b7b10d6..0ac9a9567 100644 --- a/frontend/tailwind.config.cjs +++ b/frontend/tailwind.config.cjs @@ -2,6 +2,19 @@ module.exports = { content: ["./src/**/*.{html,ts,tsx}"], darkMode: "class", - theme: {}, + theme: { + extend: { + keyframes: { + "linear-indefinite": { + "0%": { transform: "translateX(-100%) scaleX(1)" }, + "50%": { transform: "translateX(0%) scaleX(0.25)" }, + "100%": { transform: "translateX(100%)" }, + }, + }, + animation: { + "linear-indefinite": "linear-indefinite 3s cubic-bezier(1, 0.1, 0.1, 1) infinite", + }, + } + }, plugins: [], }; From 9ef99d4a7d543cde6d5247de1a7e3efd3cb4c2ab Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 12 Oct 2023 11:55:56 +0200 Subject: [PATCH 4/9] Removed debug code --- frontend/src/modules/MyModule/view.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/frontend/src/modules/MyModule/view.tsx b/frontend/src/modules/MyModule/view.tsx index a104a3b51..21d99a6ad 100644 --- a/frontend/src/modules/MyModule/view.tsx +++ b/frontend/src/modules/MyModule/view.tsx @@ -2,7 +2,6 @@ import React from "react"; import Plot from "react-plotly.js"; import { ModuleFCProps } from "@framework/Module"; -import { ViewStatusWriter, useViewStatusWriter } from "@framework/StatusWriter"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ColorScaleType } from "@lib/utils/ColorScale"; @@ -404,25 +403,14 @@ for (let i = 0; i < countryData.length; i += 2) { } export const view = (props: ModuleFCProps) => { - console.debug("rendering mymodule"); const type = props.moduleContext.useStoreValue("type"); const gradientType = props.moduleContext.useStoreValue("gradientType"); const min = props.moduleContext.useStoreValue("min"); const max = props.moduleContext.useStoreValue("max"); const divMidPoint = props.moduleContext.useStoreValue("divMidPoint"); - const statusWriter = useViewStatusWriter(props.moduleContext); - const ref = React.useRef(null); - if (type === ColorScaleType.Continuous) { - statusWriter.addError("Continuous selected"); - } - - if (gradientType === "diverging") { - statusWriter.addWarning("Diverging selected"); - } - const size = useElementSize(ref); const colorScale = From d7e484d30cd507f48722cf7cf8bd1fc180e3efc8 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 12 Oct 2023 12:14:53 +0200 Subject: [PATCH 5/9] Removed unused code and renamed status hook --- frontend/src/framework/ModuleContext.ts | 11 ----------- .../internal/ModuleInstanceStatusControllerPrivate.ts | 2 +- .../ViewWrapper/private-components/header.tsx | 6 +++--- .../components/DebugProfiler/debugProfiler.tsx | 6 +++--- .../src/modules/SimulationTimeSeriesMatrix/view.tsx | 2 +- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index fa45a4d13..e1453da03 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -6,13 +6,6 @@ import { ModuleInstanceStatusController } from "./ModuleInstanceStatusController import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; -export enum ModuleInstanceState { - LOADING, - READY, - ERROR, - WARNING, -} - export class ModuleContext { private _moduleInstance: ModuleInstance; private _stateStore: StateStore; @@ -68,8 +61,4 @@ export class ModuleContext { getModuleInstanceStatusController(): ModuleInstanceStatusController { return this._moduleInstance.getStatusController(); } - - getSource(): string { - throw Error("Not implemented"); - } } diff --git a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts index 7e07bd8e3..48463a775 100644 --- a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts +++ b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts @@ -67,7 +67,7 @@ export class ModuleInstanceStatusControllerPrivate extends ModuleInstanceStatusC } } -export function useStatusControllerValue( +export function useStatusControllerStateValue( statusController: ModuleInstanceStatusControllerPrivate, stateKey: T ): StatusControllerState[T] { diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx index 77e1aab27..3747de730 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx @@ -4,7 +4,7 @@ import ReactDOM from "react-dom"; import { ModuleInstance } from "@framework/ModuleInstance"; import { StatusMessageType } from "@framework/ModuleInstanceStatusController"; import { SyncSettingKey, SyncSettingsMeta } from "@framework/SyncSettings"; -import { useStatusControllerValue } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; +import { useStatusControllerStateValue } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; import { Badge } from "@lib/components/Badge"; import { CircularProgress } from "@lib/components/CircularProgress"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; @@ -24,8 +24,8 @@ export const Header: React.FC = (props) => { props.moduleInstance.getSyncedSettingKeys() ); const [title, setTitle] = React.useState(props.moduleInstance.getTitle()); - const isLoading = useStatusControllerValue(props.moduleInstance.getStatusController(), "loading"); - const statusMessages = useStatusControllerValue(props.moduleInstance.getStatusController(), "messages"); + const isLoading = useStatusControllerStateValue(props.moduleInstance.getStatusController(), "loading"); + const statusMessages = useStatusControllerStateValue(props.moduleInstance.getStatusController(), "messages"); const [statusMessagesVisible, setStatusMessagesVisible] = React.useState(false); const ref = React.useRef(null); diff --git a/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx b/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx index 2f77ab8f8..2be384abb 100644 --- a/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx +++ b/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx @@ -3,7 +3,7 @@ import React from "react"; import { StatusSource } from "@framework/ModuleInstanceStatusController"; import { ModuleInstanceStatusControllerPrivate, - useStatusControllerValue, + useStatusControllerStateValue, } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; import { isDevMode } from "@lib/utils/devMode"; @@ -61,11 +61,11 @@ type RenderInfo = { export const DebugProfiler: React.FC = (props) => { const [renderInfo, setRenderInfo] = React.useState(null); - const reportedRenderCount = useStatusControllerValue( + const reportedRenderCount = useStatusControllerStateValue( props.statusController, props.source === StatusSource.View ? "viewRenderCount" : "settingsRenderCount" ); - const customDebugMessage = useStatusControllerValue( + const customDebugMessage = useStatusControllerStateValue( props.statusController, props.source === StatusSource.View ? "viewDebugMessage" : "settingsDebugMessage" ); diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx index 433be773b..102fd5868 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx @@ -4,7 +4,7 @@ import Plot from "react-plotly.js"; import { Ensemble } from "@framework/Ensemble"; import { EnsembleIdent } from "@framework/EnsembleIdent"; import { ModuleFCProps } from "@framework/Module"; -import { ViewStatusWriter, useViewStatusWriter } from "@framework/StatusWriter"; +import { useViewStatusWriter } from "@framework/StatusWriter"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ColorScaleGradientType } from "@lib/utils/ColorScale"; From 1d94fdfe2ea5b6b09fa54446d444f16adc490e78 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 12 Oct 2023 12:59:29 +0200 Subject: [PATCH 6/9] Removed more debug code and adjusted names --- .../private-components/tag.tsx | 2 ++ .../SimulationTimeSeriesMatrix/view.tsx | 26 ++++++++----------- .../contentMessage.tsx} | 2 +- .../components/ContentMessage/index.ts | 1 + .../components/MessageContent/index.ts | 1 - .../unit-tests/EnsembleParameters.test.ts | 2 ++ 6 files changed, 17 insertions(+), 17 deletions(-) rename frontend/src/modules/_shared/components/{MessageContent/messageContent.tsx => ContentMessage/contentMessage.tsx} (96%) create mode 100644 frontend/src/modules/_shared/components/ContentMessage/index.ts delete mode 100644 frontend/src/modules/_shared/components/MessageContent/index.ts diff --git a/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx b/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx index 7159fbd07..6c2bbe420 100644 --- a/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx +++ b/frontend/src/lib/components/SmartNodeSelector/private-components/tag.tsx @@ -3,6 +3,8 @@ import React from "react"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { Close, Error, ExpandLess, ExpandMore, Help, Warning } from "@mui/icons-material"; +import "animate.css"; + import { TreeNodeSelection } from "../private-utils/treeNodeSelection"; type TagProps = { diff --git a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx index 102fd5868..62f421ad8 100644 --- a/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx +++ b/frontend/src/modules/SimulationTimeSeriesMatrix/view.tsx @@ -8,7 +8,7 @@ import { useViewStatusWriter } from "@framework/StatusWriter"; import { useEnsembleSet } from "@framework/WorkbenchSession"; import { useElementSize } from "@lib/hooks/useElementSize"; import { ColorScaleGradientType } from "@lib/utils/ColorScale"; -import { ContentError } from "@modules/_shared/components/MessageContent/messageContent"; +import { ContentError } from "@modules/_shared/components/ContentMessage"; import { useHistoricalVectorDataQueries, useStatisticalVectorDataQueries, useVectorDataQueries } from "./queryHooks"; import { GroupBy, State, VisualizationMode } from "./state"; @@ -68,22 +68,18 @@ export const view = ({ moduleContext, workbenchSession, workbenchSettings }: Mod vectorSpecificationsWithHistoricalData?.some((vec) => vec.hasHistoricalVector) ?? false ); - const isQueryFetching = [ - ...vectorDataQueries.filter((query) => query.isFetching), - ...vectorStatisticsQueries.filter((query) => query.isFetching), - ...historicalVectorDataQueries.filter((query) => query.isFetching), - ]; + const isQueryFetching = + vectorDataQueries.some((query) => query.isFetching) || + vectorStatisticsQueries.some((query) => query.isFetching) || + historicalVectorDataQueries.some((query) => query.isFetching); - statusWriter.setLoading(isQueryFetching.length > 0); + statusWriter.setLoading(isQueryFetching); - statusWriter.setDebugMessage("bla"); - - const hasQueryError = [ - ...vectorDataQueries.filter((query) => query.isError), - ...vectorStatisticsQueries.filter((query) => query.isError), - ...historicalVectorDataQueries.filter((query) => query.isError), - ]; - if (hasQueryError.length > 0) { + const hasQueryError = + vectorDataQueries.some((query) => query.isError) || + vectorStatisticsQueries.some((query) => query.isError) || + historicalVectorDataQueries.some((query) => query.isError); + if (hasQueryError) { statusWriter.addError("One or more queries have an error state."); return One or more queries have an error state.; } diff --git a/frontend/src/modules/_shared/components/MessageContent/messageContent.tsx b/frontend/src/modules/_shared/components/ContentMessage/contentMessage.tsx similarity index 96% rename from frontend/src/modules/_shared/components/MessageContent/messageContent.tsx rename to frontend/src/modules/_shared/components/ContentMessage/contentMessage.tsx index 411fd6c3d..5c7682caa 100644 --- a/frontend/src/modules/_shared/components/MessageContent/messageContent.tsx +++ b/frontend/src/modules/_shared/components/ContentMessage/contentMessage.tsx @@ -24,7 +24,7 @@ export const ContentMessage: React.FC = (props) => { ); }; -ContentMessage.displayName = "MessageContent"; +ContentMessage.displayName = "ContentMessage"; export type ContentErrorProps = { children: React.ReactNode; diff --git a/frontend/src/modules/_shared/components/ContentMessage/index.ts b/frontend/src/modules/_shared/components/ContentMessage/index.ts new file mode 100644 index 000000000..496fdc502 --- /dev/null +++ b/frontend/src/modules/_shared/components/ContentMessage/index.ts @@ -0,0 +1 @@ +export { ContentInfo, ContentError } from "./contentMessage"; diff --git a/frontend/src/modules/_shared/components/MessageContent/index.ts b/frontend/src/modules/_shared/components/MessageContent/index.ts deleted file mode 100644 index 97cff6270..000000000 --- a/frontend/src/modules/_shared/components/MessageContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ContentInfo, ContentError } from "./messageContent"; diff --git a/frontend/tests/unit-tests/EnsembleParameters.test.ts b/frontend/tests/unit-tests/EnsembleParameters.test.ts index 53e7e7572..a9f22d2fe 100644 --- a/frontend/tests/unit-tests/EnsembleParameters.test.ts +++ b/frontend/tests/unit-tests/EnsembleParameters.test.ts @@ -12,6 +12,7 @@ const PARAM_ARR: Parameter[] = [ {type: ParameterType.DISCRETE, name: "dparam_B", groupName: null, description: "descB", isConstant: false, realizations: [1,2,3], values: ["A", "B", "C"]}, ]; + describe("EnsembleParameters tests", () => { test("Get list of parameter idents", () => { const ensParams = new EnsembleParameters(PARAM_ARR); @@ -97,6 +98,7 @@ describe("EnsembleParameters tests", () => { }); }); + describe("ParameterIdent tests", () => { test("Conversion to/from string", () => { { From b56e4b6096386bbf8c1991ed4263a3a827c70c77 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 13 Oct 2023 10:30:59 +0200 Subject: [PATCH 7/9] Adjusted according to review discussion --- frontend/src/framework/ModuleContext.ts | 4 +- frontend/src/framework/ModuleInstance.ts | 8 +- .../ModuleInstanceStatusController.ts | 81 +--------- frontend/src/framework/StatusWriter.ts | 79 ++++++++++ frontend/src/framework/StatusWriter.tsx | 77 ---------- .../ModuleInstanceStatusControllerInternal.ts | 139 ++++++++++++++++++ .../ModuleInstanceStatusControllerPrivate.ts | 80 ---------- .../ViewWrapper/private-components/header.tsx | 2 +- .../DebugProfiler/debugProfiler.tsx | 6 +- 9 files changed, 235 insertions(+), 241 deletions(-) create mode 100644 frontend/src/framework/StatusWriter.ts delete mode 100644 frontend/src/framework/StatusWriter.tsx create mode 100644 frontend/src/framework/internal/ModuleInstanceStatusControllerInternal.ts delete mode 100644 frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index e1453da03..3eb2c2c0d 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -2,9 +2,9 @@ import React from "react"; import { BroadcastChannel } from "./Broadcaster"; import { ModuleInstance } from "./ModuleInstance"; -import { ModuleInstanceStatusController } from "./ModuleInstanceStatusController"; import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; +import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal"; export class ModuleContext { private _moduleInstance: ModuleInstance; @@ -58,7 +58,7 @@ export class ModuleContext { this._moduleInstance.setTitle(title); } - getModuleInstanceStatusController(): ModuleInstanceStatusController { + getStatusController(): ModuleInstanceStatusControllerInternal { return this._moduleInstance.getStatusController(); } } diff --git a/frontend/src/framework/ModuleInstance.ts b/frontend/src/framework/ModuleInstance.ts index c53454484..d94dd1d65 100644 --- a/frontend/src/framework/ModuleInstance.ts +++ b/frontend/src/framework/ModuleInstance.ts @@ -9,7 +9,7 @@ import { ModuleContext } from "./ModuleContext"; import { StateBaseType, StateOptions, StateStore } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; import { Workbench } from "./Workbench"; -import { ModuleInstanceStatusControllerPrivate } from "./internal/ModuleInstanceStatusControllerPrivate"; +import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal"; export enum ModuleInstanceState { INITIALIZING, @@ -36,7 +36,7 @@ export class ModuleInstance { private _cachedDefaultState: StateType | null; private _cachedStateStoreOptions?: StateOptions; private _initialSettings: InitialSettings | null; - private _statusController: ModuleInstanceStatusControllerPrivate; + private _statusController: ModuleInstanceStatusControllerInternal; constructor( module: Module, @@ -59,7 +59,7 @@ export class ModuleInstance { this._fatalError = null; this._cachedDefaultState = null; this._initialSettings = null; - this._statusController = new ModuleInstanceStatusControllerPrivate(); + this._statusController = new ModuleInstanceStatusControllerInternal(); this._broadcastChannels = {} as Record; @@ -181,7 +181,7 @@ export class ModuleInstance { return this._module; } - getStatusController(): ModuleInstanceStatusControllerPrivate { + getStatusController(): ModuleInstanceStatusControllerInternal { return this._statusController; } diff --git a/frontend/src/framework/ModuleInstanceStatusController.ts b/frontend/src/framework/ModuleInstanceStatusController.ts index 65ad8de96..e63d1f2bc 100644 --- a/frontend/src/framework/ModuleInstanceStatusController.ts +++ b/frontend/src/framework/ModuleInstanceStatusController.ts @@ -1,5 +1,3 @@ -import { cloneDeep } from "lodash"; - export enum StatusMessageType { Warning = "warning", Error = "error", @@ -10,78 +8,13 @@ export enum StatusSource { Settings = "settings", } -export type StatusMessage = { - source: StatusSource; - message: string; - type: StatusMessageType; -}; - -export type StatusControllerState = { - messages: StatusMessage[]; - loading: boolean; - viewDebugMessage: string; - settingsDebugMessage: string; - viewRenderCount: number | null; - settingsRenderCount: number | null; -}; - -export class ModuleInstanceStatusController { - protected _stateCandidates: StatusControllerState; - protected _state: StatusControllerState; - - constructor() { - this._state = { - messages: [], - loading: false, - viewDebugMessage: "", - settingsDebugMessage: "", - viewRenderCount: null, - settingsRenderCount: null, - }; - this._stateCandidates = cloneDeep(this._state); - } - - addMessage(source: StatusSource, message: string, type: StatusMessageType): void { - this._stateCandidates.messages.push({ - source, - message, - type, - }); - } - - clearMessages(source: StatusSource): void { - this._stateCandidates.messages = this._stateCandidates.messages.filter((msg) => msg.source !== source); - } - - setLoading(isLoading: boolean): void { - this._stateCandidates.loading = isLoading; - } - - setDebugMessage(source: StatusSource, message: string): void { - if (source === StatusSource.View) { - this._stateCandidates.viewDebugMessage = message; - } - if (source === StatusSource.Settings) { - this._stateCandidates.settingsDebugMessage = message; - } - } +export interface ModuleInstanceStatusController { + addMessage(source: StatusSource, message: string, type: StatusMessageType): void; + clearMessages(source: StatusSource): void; + setLoading(isLoading: boolean): void; - incrementReportedComponentRenderCount(source: StatusSource): void { - if (source === StatusSource.View) { - if (this._stateCandidates.viewRenderCount === null) { - this._stateCandidates.viewRenderCount = 0; - } - this._stateCandidates.viewRenderCount++; - } - if (source === StatusSource.Settings) { - if (this._stateCandidates.settingsRenderCount === null) { - this._stateCandidates.settingsRenderCount = 0; - } - this._stateCandidates.settingsRenderCount++; - } - } + setDebugMessage(source: StatusSource, message: string): void; + incrementReportedComponentRenderCount(source: StatusSource): void; - reviseState(): void { - this._state = cloneDeep(this._stateCandidates); - } + reviseAndPublishState(): void; } diff --git a/frontend/src/framework/StatusWriter.ts b/frontend/src/framework/StatusWriter.ts new file mode 100644 index 000000000..1169b1fc5 --- /dev/null +++ b/frontend/src/framework/StatusWriter.ts @@ -0,0 +1,79 @@ +import React from "react"; + +import { ModuleContext } from "./ModuleContext"; +import { StatusMessageType, StatusSource } from "./ModuleInstanceStatusController"; +import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal"; + +export class ViewStatusWriter { + private _statusController: ModuleInstanceStatusControllerInternal; + + constructor(statusController: ModuleInstanceStatusControllerInternal) { + this._statusController = statusController; + } + + setLoading(isLoading: boolean): void { + this._statusController.setLoading(isLoading); + } + + addError(message: string): void { + this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Error); + } + + addWarning(message: string): void { + this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Warning); + } + + setDebugMessage(message: string): void { + this._statusController.setDebugMessage(StatusSource.View, message); + } +} + +export class SettingsStatusWriter { + private _statusController: ModuleInstanceStatusControllerInternal; + + constructor(statusController: ModuleInstanceStatusControllerInternal) { + this._statusController = statusController; + } + + addError(message: string): void { + this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Error); + } + + addWarning(message: string): void { + this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Warning); + } + + setDebugMessage(message: string): void { + this._statusController.setDebugMessage(StatusSource.Settings, message); + } +} + +export function useViewStatusWriter(moduleContext: ModuleContext): ViewStatusWriter { + const statusController = moduleContext.getStatusController(); + + const statusWriter = React.useRef(new ViewStatusWriter(statusController)); + + statusController.clearMessages(StatusSource.View); + statusController.incrementReportedComponentRenderCount(StatusSource.View); + + React.useEffect(function handleRender() { + statusController.reviseAndPublishState(); + }); + + return statusWriter.current; +} + +export function useSettingsStatusWriter(moduleContext: ModuleContext): ViewStatusWriter { + const statusController = moduleContext.getStatusController(); + + const statusWriter = React.useRef(new ViewStatusWriter(statusController)); + + statusController.clearMessages(StatusSource.Settings); + statusController.incrementReportedComponentRenderCount(StatusSource.Settings); + + React.useEffect(function handleRender() { + statusController.reviseAndPublishState(); + }); + + return statusWriter.current; +} diff --git a/frontend/src/framework/StatusWriter.tsx b/frontend/src/framework/StatusWriter.tsx deleted file mode 100644 index 3bb0b4262..000000000 --- a/frontend/src/framework/StatusWriter.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from "react"; - -import { ModuleContext } from "./ModuleContext"; -import { ModuleInstanceStatusController, StatusMessageType, StatusSource } from "./ModuleInstanceStatusController"; - -export class ViewStatusWriter { - private _statusController: ModuleInstanceStatusController; - - constructor(moduleContext: ModuleContext) { - this._statusController = moduleContext.getModuleInstanceStatusController(); - } - - setLoading(isLoading: boolean): void { - this._statusController.setLoading(isLoading); - } - - addError(message: string): void { - this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Error); - } - - addWarning(message: string): void { - this._statusController.addMessage(StatusSource.View, message, StatusMessageType.Warning); - } - - setDebugMessage(message: string): void { - this._statusController.setDebugMessage(StatusSource.View, message); - } -} - -export class SettingsStatusWriter { - private _statusController: ModuleInstanceStatusController; - - constructor(moduleContext: ModuleContext) { - this._statusController = moduleContext.getModuleInstanceStatusController(); - } - - addError(message: string): void { - this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Error); - } - - addWarning(message: string): void { - this._statusController.addMessage(StatusSource.Settings, message, StatusMessageType.Warning); - } - - setDebugMessage(message: string): void { - this._statusController.setDebugMessage(StatusSource.Settings, message); - } -} - -function useStatusWriter< - T extends StatusSource, - TStatusWriter = T extends StatusSource.View ? ViewStatusWriter : SettingsStatusWriter ->(moduleContext: ModuleContext, statusSource: T): TStatusWriter { - const statusWriter = React.useRef( - (statusSource === StatusSource.View - ? new ViewStatusWriter(moduleContext) - : new SettingsStatusWriter(moduleContext)) as TStatusWriter - ); - - const statusController = moduleContext.getModuleInstanceStatusController(); - statusController.clearMessages(statusSource); - statusController.incrementReportedComponentRenderCount(statusSource); - - React.useEffect(function handleRender() { - statusController.reviseState(); - }); - - return statusWriter.current; -} - -export function useViewStatusWriter(moduleContext: ModuleContext): ViewStatusWriter { - return useStatusWriter(moduleContext, StatusSource.View); -} - -export function useSettingsStatusWriter(moduleContext: ModuleContext): SettingsStatusWriter { - return useStatusWriter(moduleContext, StatusSource.Settings); -} diff --git a/frontend/src/framework/internal/ModuleInstanceStatusControllerInternal.ts b/frontend/src/framework/internal/ModuleInstanceStatusControllerInternal.ts new file mode 100644 index 000000000..bc3c01de0 --- /dev/null +++ b/frontend/src/framework/internal/ModuleInstanceStatusControllerInternal.ts @@ -0,0 +1,139 @@ +import React from "react"; + +import { + ModuleInstanceStatusController, + StatusMessageType, + StatusSource, +} from "@framework/ModuleInstanceStatusController"; + +import { cloneDeep, filter, isEqual, keys } from "lodash"; + +type StatusMessage = { + source: StatusSource; + message: string; + type: StatusMessageType; +}; + +type StatusControllerState = { + messages: StatusMessage[]; + loading: boolean; + viewDebugMessage: string; + settingsDebugMessage: string; + viewRenderCount: number | null; + settingsRenderCount: number | null; +}; + +export class ModuleInstanceStatusControllerInternal implements ModuleInstanceStatusController { + protected _stateCandidates: StatusControllerState; + protected _state: StatusControllerState = { + messages: [], + loading: false, + viewDebugMessage: "", + settingsDebugMessage: "", + viewRenderCount: null, + settingsRenderCount: null, + }; + private _subscribers: Map void>> = new Map(); + + constructor() { + this._stateCandidates = cloneDeep(this._state); + } + + addMessage(source: StatusSource, message: string, type: StatusMessageType): void { + this._stateCandidates.messages.push({ + source, + message, + type, + }); + } + + clearMessages(source: StatusSource): void { + this._stateCandidates.messages = this._stateCandidates.messages.filter((msg) => msg.source !== source); + } + + setLoading(isLoading: boolean): void { + this._stateCandidates.loading = isLoading; + } + + setDebugMessage(source: StatusSource, message: string): void { + if (source === StatusSource.View) { + this._stateCandidates.viewDebugMessage = message; + } + if (source === StatusSource.Settings) { + this._stateCandidates.settingsDebugMessage = message; + } + } + + incrementReportedComponentRenderCount(source: StatusSource): void { + if (source === StatusSource.View) { + if (this._stateCandidates.viewRenderCount === null) { + this._stateCandidates.viewRenderCount = 0; + } + this._stateCandidates.viewRenderCount++; + } + if (source === StatusSource.Settings) { + if (this._stateCandidates.settingsRenderCount === null) { + this._stateCandidates.settingsRenderCount = 0; + } + this._stateCandidates.settingsRenderCount++; + } + } + + reviseAndPublishState(): void { + const differentStateKeys = filter(keys(this._stateCandidates), (key: keyof StatusControllerState) => { + return !isEqual(this._state[key], this._stateCandidates[key]); + }) as (keyof StatusControllerState)[]; + + this._state = cloneDeep(this._stateCandidates); + + differentStateKeys.forEach((stateKey) => { + this.notifySubscribers(stateKey); + }); + } + + private notifySubscribers(stateKey: keyof StatusControllerState): void { + const subscribers = this._subscribers.get(stateKey); + if (subscribers) { + subscribers.forEach((subscriber) => { + subscriber(); + }); + } + } + + makeSnapshotGetter(stateKey: T): () => StatusControllerState[T] { + const snapshotGetter = (): any => { + return this._state[stateKey]; + }; + + return snapshotGetter; + } + + makeSubscriberFunction( + stateKey: T + ): (onStoreChangeCallback: () => void) => () => void { + // Using arrow function in order to keep "this" in context + const subscriber = (onStoreChangeCallback: () => void): (() => void) => { + const subscribers = this._subscribers.get(stateKey) || new Set(); + subscribers.add(onStoreChangeCallback); + this._subscribers.set(stateKey, subscribers); + + return () => { + subscribers.delete(onStoreChangeCallback); + }; + }; + + return subscriber; + } +} + +export function useStatusControllerStateValue( + statusController: ModuleInstanceStatusControllerInternal, + stateKey: T +): StatusControllerState[T] { + const value = React.useSyncExternalStore( + statusController.makeSubscriberFunction(stateKey), + statusController.makeSnapshotGetter(stateKey) + ); + + return value; +} diff --git a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts b/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts deleted file mode 100644 index 48463a775..000000000 --- a/frontend/src/framework/internal/ModuleInstanceStatusControllerPrivate.ts +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; - -import { - ModuleInstanceStatusController, - StatusControllerState, - StatusSource, -} from "@framework/ModuleInstanceStatusController"; - -import { filter, isEqual, keys } from "lodash"; - -export class ModuleInstanceStatusControllerPrivate extends ModuleInstanceStatusController { - private _subscribers: Map void>>; - - constructor() { - super(); - this._subscribers = new Map(); - } - - clearMessages(source: StatusSource): void { - super.clearMessages(source); - } - - reviseState(): void { - const differentStateKeys = filter(keys(this._stateCandidates), (key: keyof StatusControllerState) => { - return !isEqual(this._state[key], this._stateCandidates[key]); - }) as (keyof StatusControllerState)[]; - - super.reviseState(); - - differentStateKeys.forEach((stateKey) => { - this.notifySubscribers(stateKey); - }); - } - - private notifySubscribers(stateKey: keyof StatusControllerState): void { - const subscribers = this._subscribers.get(stateKey); - if (subscribers) { - subscribers.forEach((subscriber) => { - subscriber(); - }); - } - } - - makeSnapshotGetter(stateKey: T): () => StatusControllerState[T] { - const snapshotGetter = (): any => { - return this._state[stateKey]; - }; - - return snapshotGetter; - } - - makeSubscriberFunction( - stateKey: T - ): (onStoreChangeCallback: () => void) => () => void { - // Using arrow function in order to keep "this" in context - const subscriber = (onStoreChangeCallback: () => void): (() => void) => { - const subscribers = this._subscribers.get(stateKey) || new Set(); - subscribers.add(onStoreChangeCallback); - this._subscribers.set(stateKey, subscribers); - - return () => { - subscribers.delete(onStoreChangeCallback); - }; - }; - - return subscriber; - } -} - -export function useStatusControllerStateValue( - statusController: ModuleInstanceStatusControllerPrivate, - stateKey: T -): StatusControllerState[T] { - const value = React.useSyncExternalStore( - statusController.makeSubscriberFunction(stateKey), - statusController.makeSnapshotGetter(stateKey) - ); - - return value; -} diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx index 3747de730..4c4960f2a 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/header.tsx @@ -4,7 +4,7 @@ import ReactDOM from "react-dom"; import { ModuleInstance } from "@framework/ModuleInstance"; import { StatusMessageType } from "@framework/ModuleInstanceStatusController"; import { SyncSettingKey, SyncSettingsMeta } from "@framework/SyncSettings"; -import { useStatusControllerStateValue } from "@framework/internal/ModuleInstanceStatusControllerPrivate"; +import { useStatusControllerStateValue } from "@framework/internal/ModuleInstanceStatusControllerInternal"; import { Badge } from "@lib/components/Badge"; import { CircularProgress } from "@lib/components/CircularProgress"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; diff --git a/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx b/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx index 2be384abb..9d8b373c9 100644 --- a/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx +++ b/frontend/src/framework/internal/components/DebugProfiler/debugProfiler.tsx @@ -2,9 +2,9 @@ import React from "react"; import { StatusSource } from "@framework/ModuleInstanceStatusController"; import { - ModuleInstanceStatusControllerPrivate, + ModuleInstanceStatusControllerInternal, useStatusControllerStateValue, -} from "@framework/internal/ModuleInstanceStatusControllerPrivate"; +} from "@framework/internal/ModuleInstanceStatusControllerInternal"; import { isDevMode } from "@lib/utils/devMode"; type DebugProfilerRenderInfoProps = { @@ -41,7 +41,7 @@ DebugProfilerWrapper.displayName = "DebugProfilerWrapper"; export type DebugProfilerProps = { id: string; children: React.ReactNode; - statusController: ModuleInstanceStatusControllerPrivate; + statusController: ModuleInstanceStatusControllerInternal; source: StatusSource; }; From 81ab7eccebaf0d71aea39474bdc8270051cf7ed5 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 13 Oct 2023 11:17:48 +0200 Subject: [PATCH 8/9] Fixed wrong type in `ModuleContext` --- frontend/src/framework/ModuleContext.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/framework/ModuleContext.ts b/frontend/src/framework/ModuleContext.ts index 3eb2c2c0d..6bc70e309 100644 --- a/frontend/src/framework/ModuleContext.ts +++ b/frontend/src/framework/ModuleContext.ts @@ -2,9 +2,9 @@ import React from "react"; import { BroadcastChannel } from "./Broadcaster"; import { ModuleInstance } from "./ModuleInstance"; +import { ModuleInstanceStatusController } from "./ModuleInstanceStatusController"; import { StateBaseType, StateStore, useSetStoreValue, useStoreState, useStoreValue } from "./StateStore"; import { SyncSettingKey } from "./SyncSettings"; -import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal"; export class ModuleContext { private _moduleInstance: ModuleInstance; @@ -58,7 +58,7 @@ export class ModuleContext { this._moduleInstance.setTitle(title); } - getStatusController(): ModuleInstanceStatusControllerInternal { + getStatusController(): ModuleInstanceStatusController { return this._moduleInstance.getStatusController(); } } From 4c6c2ff0c7b810a2b340dc6bef2e85bb7048cc23 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 13 Oct 2023 11:32:07 +0200 Subject: [PATCH 9/9] Fixed wrong type --- frontend/src/framework/StatusWriter.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/framework/StatusWriter.ts b/frontend/src/framework/StatusWriter.ts index 1169b1fc5..a71aa4266 100644 --- a/frontend/src/framework/StatusWriter.ts +++ b/frontend/src/framework/StatusWriter.ts @@ -1,13 +1,12 @@ import React from "react"; import { ModuleContext } from "./ModuleContext"; -import { StatusMessageType, StatusSource } from "./ModuleInstanceStatusController"; -import { ModuleInstanceStatusControllerInternal } from "./internal/ModuleInstanceStatusControllerInternal"; +import { ModuleInstanceStatusController, StatusMessageType, StatusSource } from "./ModuleInstanceStatusController"; export class ViewStatusWriter { - private _statusController: ModuleInstanceStatusControllerInternal; + private _statusController: ModuleInstanceStatusController; - constructor(statusController: ModuleInstanceStatusControllerInternal) { + constructor(statusController: ModuleInstanceStatusController) { this._statusController = statusController; } @@ -29,9 +28,9 @@ export class ViewStatusWriter { } export class SettingsStatusWriter { - private _statusController: ModuleInstanceStatusControllerInternal; + private _statusController: ModuleInstanceStatusController; - constructor(statusController: ModuleInstanceStatusControllerInternal) { + constructor(statusController: ModuleInstanceStatusController) { this._statusController = statusController; }