From 20d8d313f000245ce9bfadfcdd8a108d7e0a1efc Mon Sep 17 00:00:00 2001 From: Martin Karkowski Date: Tue, 20 Apr 2021 20:49:15 +0200 Subject: [PATCH] Fixing Bridge --- lib/communication/bridge.ts | 57 ++++--------- .../mirrors/ioSocketMirrorServer.ts | 14 +-- lib/module/BaseModule.ts | 6 +- lib/types/nope/nopeCommunication.interface.ts | 10 +-- lib/types/nope/nopeModule.interface.ts | 2 + modules/wamo/src/wamo.basemodule.module.ts | 85 ++++++++++--------- modules/wamo/src/wamo.carrierMapper.module.ts | 35 ++++---- .../src/xetics.module.ts | 17 ++-- .../admin-shell/instance/InstanceDetails.tsx | 72 ++++++++++++++-- resources/ui/iframe.tsx | 80 +++++++++++++++++ .../ui/layout/interfaces/IModalSettings.ts | 22 ++--- resources/ui/popup/component.tsx | 72 ++++++++++++++++ .../ui/popup/interfaces/IModalSettings.ts | 26 ++++++ 13 files changed, 368 insertions(+), 130 deletions(-) create mode 100644 resources/ui/iframe.tsx create mode 100644 resources/ui/popup/component.tsx create mode 100644 resources/ui/popup/interfaces/IModalSettings.ts diff --git a/lib/communication/bridge.ts b/lib/communication/bridge.ts index 8e02280..9445e96 100644 --- a/lib/communication/bridge.ts +++ b/lib/communication/bridge.ts @@ -63,11 +63,11 @@ const UNSPECIFIC_TO_METHODS: { subscribe: keyof ICommunicationInterface; }; } = { - aurevoir: { + Aurevoir: { emit: "emitAurevoir", subscribe: "onAurevoir" }, - bonjour: { + Bonjour: { emit: "emitBonjour", subscribe: "onBonjour" }, @@ -102,7 +102,11 @@ const LAYER_METHOD_TO_EVENT: { } = { // Specific: onEvent: "event/", - emitEvent: "event/" + emitEvent: "event/", + onRpcRequest: "", + emitRpcRequest: "", + onRpcResponse: "", + emitRpcResponse: "" }; for (const eventName in UNSPECIFIC_TO_METHODS) { @@ -117,12 +121,12 @@ const MIRROR_METHOD_TO_EVENT: { [P in keyof ICommunicationInterface]: ValidEventTypesOfMirror; } = copy(LAYER_METHOD_TO_EVENT) as any; -MIRROR_METHOD_TO_EVENT.onEvent = "event"; -MIRROR_METHOD_TO_EVENT.emitEvent = "event"; -MIRROR_METHOD_TO_EVENT.onRpcRequest = "rpcRequest"; -MIRROR_METHOD_TO_EVENT.emitRpcRequest = "rpcRequest"; -MIRROR_METHOD_TO_EVENT.onRpcResponse = "rpcResponse"; -MIRROR_METHOD_TO_EVENT.emitRpcResponse = "rpcResponse"; +MIRROR_METHOD_TO_EVENT.onEvent = "Event"; +MIRROR_METHOD_TO_EVENT.emitEvent = "Event"; +MIRROR_METHOD_TO_EVENT.onRpcRequest = "RpcRequest"; +MIRROR_METHOD_TO_EVENT.emitRpcRequest = "RpcRequest"; +MIRROR_METHOD_TO_EVENT.onRpcResponse = "RpcResponse"; +MIRROR_METHOD_TO_EVENT.emitRpcResponse = "RpcResponse"; const MIRROR_EVENT_TO_EMIT: { [P in keyof ValidEventTypesOfMirror]: keyof ICommunicationInterface; @@ -143,28 +147,6 @@ const OFF_METHODS: Array = [ "offRpcResponse" ]; -const MAPPING_METHODS: { - [P in keyof ICommunicationInterface]: keyof ICommunicationInterface; -} = { - onAurevoir: "emitAurevoir", - onBonjour: "emitBonjour", - onEvent: "emitEvent", - onNewInstanceGeneratorsAvailable: "emitNewInstanceGeneratorsAvailable", - onNewInstancesAvailable: "emitNewInstancesAvailable", - onNewObservablesAvailable: "emitNewObersvablesAvailable", - onNewServicesAvailable: "emitNewServicesAvailable", - onRpcRequest: "emitRpcRequest", - onRpcResponse: "emitRpcResponse", - onTaskCancelation: "emitTaskCancelation", - onStatusUpdate: "emitStatusUpdate" -}; - -const METHODS_WITH_NAME: Array = [ - "onRpcResponse", - "onRpcRequest", - "onEvent" -]; - //@ts-ignore Ignore the Interface. Its implemented manually export class Bridge implements ICommunicationBridge { public connected: INopeObservable; @@ -243,6 +225,8 @@ export class Bridge implements ICommunicationBridge { for (const method of EMITTING_METHODS) { const eventName = LAYER_METHOD_TO_EVENT[method]; this[method] = async (data) => { + if (method !== "emitStatusUpdate") + console.log("emitting", method, eventName, data); _this._emit(eventName, null, data); }; } @@ -291,7 +275,7 @@ export class Bridge implements ICommunicationBridge { ): void { const _this = this; - if (METHODS_WITH_NAME.includes(method)) { + if (ON_SPECIFIC_METHODS.includes(method)) { layer[method as any](event, (data) => { // Define the Method to Forward data. _this._internalEmitter.emit(event, data); @@ -424,7 +408,7 @@ export class Bridge implements ICommunicationBridge { toExclude: ICommunicationInterface | ICommunicationMirror = null, dataToSend: any ): void { - if (this._logger.enabledFor(Logger.DEBUG) && eventName !== "StatusUpdate") { + if (this._logger.enabledFor(Logger.WARN) && eventName !== "StatusUpdate") { this._logger.debug("emitting", eventName, dataToSend); } // Emit the Event on the internal Layer. @@ -584,14 +568,7 @@ export class Bridge implements ICommunicationBridge { UNSPECIFIC_TO_METHODS[eventName].emit ); } - // Now Forward the Data. - console.log( - "Fowarding", - eventName, - "->", - MIRROR_EVENT_TO_EMIT[eventName] - ); _this._emit(eventName, mirror, data); }); } else { diff --git a/lib/communication/mirrors/ioSocketMirrorServer.ts b/lib/communication/mirrors/ioSocketMirrorServer.ts index ba12c3b..d9c624b 100644 --- a/lib/communication/mirrors/ioSocketMirrorServer.ts +++ b/lib/communication/mirrors/ioSocketMirrorServer.ts @@ -9,21 +9,22 @@ import * as io from "socket.io"; import { getNopeLogger } from "../../logger/getLogger"; import { LoggerLevel } from "../../logger/nopeLogger"; +import { ValidEventTypesOfMirror } from "../../types/nope/nopeCommunication.interface"; import { EventMirror } from "./eventMirror"; -const EVENTS_TO_FORWARD: Set = new Set([ +const EVENTS_TO_FORWARD: Set = new Set([ // Default emitters - "aurevoir", - "bonjour", + "Aurevoir", + "Bonjour", "NewInstanceGeneratorsAvailable", "NewInstancesAvailable", "NewObersvablesAvailable", "NewServicesAvailable", "StatusUpdate", "TaskCancelation", - "event", - "rpcRequest", - "rpcResponse" + "Event", + "RpcRequest", + "RpcResponse" ]); /** @@ -69,6 +70,7 @@ export class IoSocketMirrorServer extends EventMirror { // are forwarding the data. for (const event of EVENTS_TO_FORWARD) { client.on(event, (data) => { + if (event !== "StatusUpdate") console.log("Forwarding", event, data); _this._forward(client, event, data); }); } diff --git a/lib/module/BaseModule.ts b/lib/module/BaseModule.ts index 9009d87..7a9d1ad 100644 --- a/lib/module/BaseModule.ts +++ b/lib/module/BaseModule.ts @@ -141,8 +141,11 @@ export class NopeBaseModule implements INopeModule { this.identifier = null; this._registeredFunctions = new Map(); this._registeredProperties = new Map(); + this.uiLinks = []; } + public uiLinks: { name: string; description: string; link: string }[]; + /** * Helper Function to register an Observable (a Property.) * @@ -401,7 +404,8 @@ export class NopeBaseModule implements INopeModule { identifier: this.identifier, properties: this.properties, type: this.type, - version: this.version + version: this.version, + uiLinks: this.uiLinks }; return ret; diff --git a/lib/types/nope/nopeCommunication.interface.ts b/lib/types/nope/nopeCommunication.interface.ts index 53dbdc8..d9aa5aa 100644 --- a/lib/types/nope/nopeCommunication.interface.ts +++ b/lib/types/nope/nopeCommunication.interface.ts @@ -338,17 +338,17 @@ export interface ICommunicationBridge extends ICommunicationInterface { export type ValidEventTypesOfMirror = // Default emitters - | "aurevoir" - | "bonjour" + | "Aurevoir" + | "Bonjour" | "NewInstanceGeneratorsAvailable" | "NewInstancesAvailable" | "NewObersvablesAvailable" | "NewServicesAvailable" | "StatusUpdate" | "TaskCancelation" - | "event" - | "rpcRequest" - | "rpcResponse"; + | "Event" + | "RpcRequest" + | "RpcResponse"; export const ValidEventTypesOfMirror = [ // Default emitters diff --git a/lib/types/nope/nopeModule.interface.ts b/lib/types/nope/nopeModule.interface.ts index 3e9019a..a2151f4 100644 --- a/lib/types/nope/nopeModule.interface.ts +++ b/lib/types/nope/nopeModule.interface.ts @@ -81,6 +81,8 @@ export interface INopeModuleDescription { readonly functions: { [index: string]: IFunctionOptions }; readonly properties: { [index: string]: IPropertyOptions }; + + readonly uiLinks: Array<{ name: string; description: string; link: string }>; } export interface INopeModule extends INopeModuleDescription { diff --git a/modules/wamo/src/wamo.basemodule.module.ts b/modules/wamo/src/wamo.basemodule.module.ts index 8102100..f068295 100644 --- a/modules/wamo/src/wamo.basemodule.module.ts +++ b/modules/wamo/src/wamo.basemodule.module.ts @@ -192,7 +192,7 @@ export class WaMOBaseModule }) => Promise, resetPlc = true, autoStart = true, - demo = false, + demo = false ): Promise { // Define the Author. this.author = { @@ -308,50 +308,52 @@ export class WaMOBaseModule // Only if there exists a Carrier, proceed. if (value) { - - let tasks: MESTask[] = _this._xetics.availableTasks.getContent(); - - // Only if no tasks are available, try to update the - // available tasks. Thereby we check if there are new - // tasks, we didnt checked before because they werent - // present. - if (tasks.length === 0){ - tasks = await _this._xetics.updateTasks(); - } - - // We now use the perhaps updated list of tasks, to - // determine the task for our carrier. - const productToStartTaskWith = await selectProduct({ - checkedInWorkpieceCarrier: value, - tasks - }); - - // Try to assign the new Task for the Product - if ( - !demo && - productToStartTaskWith && - _this._xetics.selectTaskForProduct(productToStartTaskWith) - ) { - // We are not running in the demo-mode and we have - // a task to start, which we are able to select. - await _this.performCurrentTask(); - } else if (demo) { + if (demo) { // Demo-Mode // Wait 10 Seconds at least spend additional 0-5000 Seconds - await sleep(Math.random() * 5000 + 10*1000); + await sleep(Math.random() * 5000 + 10 * 1000); + } else { + let tasks: MESTask[] = _this._xetics.availableTasks.getContent(); + + // Only if no tasks are available, try to update the + // available tasks. Thereby we check if there are new + // tasks, we didnt checked before because they werent + // present. + if (tasks.length === 0) { + tasks = await _this._xetics.updateTasks(); + } + + // We now use the perhaps updated list of tasks, to + // determine the task for our carrier. + const productToStartTaskWith = await selectProduct({ + checkedInWorkpieceCarrier: value, + tasks + }); + + // Try to assign the new Task for the Product + if ( + productToStartTaskWith && + (await _this._xetics.selectTaskForProduct( + productToStartTaskWith + )) + ) { + // We are not running in the demo-mode and we have + // a task to start, which we are able to select. + await _this.performCurrentTask(); + } } // Now we are done or we dont have a task. - // we release the carrier and wait for a new + // we release the carrier and wait for a new // product. await _this.releaseCarrier(); - } else { // Updating the State of the System. The System // Waits for a new Carrier - const baseInformation = copy(_this.baseInformation.getContent()); - baseInformation.logical.currentState = - "waiting"; + const baseInformation = copy( + _this.baseInformation.getContent() + ); + baseInformation.logical.currentState = "waiting"; _this.baseInformation.setContent(baseInformation); } } catch (e) { @@ -729,13 +731,12 @@ export class WaMOBaseModule observers = []; }; - let first = true; + let first = true; // Todo Check if the Replica has to be adapted observers = [ // Subscribe to the connectorReplica of the Process Module. _this._processModule.connector.subscribe((value) => { - // If we have waited to finish a Task, we are able // to proceed with the task. if (value.active == false && first == false) { @@ -745,13 +746,15 @@ export class WaMOBaseModule } // Make shure the module just tells ous - // that the task has been started. + // that the task has been started. if (value.active) { first = false; } }), - _this._plc.dynamicInstanceProperties["input.GVL_IOS.bInputTasterQuittieren"].subscribe((value) => { - console.log("bInputTasterQuittieren =>" ,value); + _this._plc.dynamicInstanceProperties[ + "input.GVL_IOS.bInputTasterQuittieren" + ].subscribe((value) => { + console.log("bInputTasterQuittieren =>", value); if (value) { // Unsubscribe the Observer clearObservers(); @@ -771,7 +774,7 @@ export class WaMOBaseModule this.baseInformation.setContent(copy(baseInformation)); // Enable the Port Replicas. - const connector = _this.connector.getContent(); + const connector = _this.connector.getContent(); connector.start = true; _this.connector.setContent(copy(connector)); diff --git a/modules/wamo/src/wamo.carrierMapper.module.ts b/modules/wamo/src/wamo.carrierMapper.module.ts index 63b5b23..eef496d 100644 --- a/modules/wamo/src/wamo.carrierMapper.module.ts +++ b/modules/wamo/src/wamo.carrierMapper.module.ts @@ -93,17 +93,19 @@ export class WaMOCarrierMapper // Define the File Name. It is based on the identifier. this._file = join(_path, this.identifier + "_file.json"); + this.carriers.setContent([]); + this.mapping.setContent({}); + // Now Test if the File exists: if (!(await exists(this._file))) { + this._logger.warn( + "No configuration File defined. Creating a new one under:", + this._file + ); - this._logger.warn("No configuration File defined. Creating a new one under:",this._file); - - this.carriers.setContent([]); - this.mapping.setContent({}); - // Create an Empty File. await this.storeConfig(); - } + } // We now try to load the configuration const loadedConfig = JSON.parse( @@ -112,12 +114,12 @@ export class WaMOCarrierMapper }) ); - // Load the Mapping and register the Carriers. - for (const carrier in loadedConfig.carriers){ + // Load the Mapping and register the Carriers. + for (const carrier in loadedConfig.carriers) { await this.registerCarrier(carrier); } // Add the Products to the Carrier. - for (const carrier in loadedConfig.mapping){ + for (const carrier in loadedConfig.mapping) { await this.addProductToCarrier(carrier, loadedConfig.mapping[carrier]); } @@ -125,18 +127,21 @@ export class WaMOCarrierMapper } /** - * Function, wich will + * Function, wich will * * @memberof WaMOCarrierMapper */ @exportMethod({ paramsHasNoCallback: true }) - public async storeConfig(){ - await createFile(this._file, JSON.stringify({ - carriers: this.carriers.getContent(), - mapping: this.mapping.getContent() - })); + public async storeConfig() { + await createFile( + this._file, + JSON.stringify({ + carriers: this.carriers.getContent(), + mapping: this.mapping.getContent() + }) + ); } /** diff --git a/modules/xetics-lean-connector/src/xetics.module.ts b/modules/xetics-lean-connector/src/xetics.module.ts index 67981d8..dd0c14a 100644 --- a/modules/xetics-lean-connector/src/xetics.module.ts +++ b/modules/xetics-lean-connector/src/xetics.module.ts @@ -241,13 +241,13 @@ export class XeticsInterfaceClient }) async startCurrentTask() { if (this.currentTask.getContent()) { - // We have to consinder, whether the job is allready active or not - if (this.taskActive.getContent()){ + if (this.taskActive.getContent()) { + console.log("already active"); return true; } - return startTask( + return await startTask( this._station, this.currentTask.getContent().id, this._user, @@ -276,8 +276,10 @@ export class XeticsInterfaceClient if (this.currentTask.getContent()) { // Call the Finish-Task Operation. Use the parameters of the current Task. // Returns the sucess of the Operation. - if (this._logger){ - this._logger.info("Finishing Task " + this.currentTask.getContent().id.toString()); + if (this._logger) { + this._logger.info( + "Finishing Task " + this.currentTask.getContent().id.toString() + ); } const finished = await finishTask( @@ -319,7 +321,7 @@ export class XeticsInterfaceClient ); let _taskActive = false; - + // Check if there exists a currently active Task // if so => assign this task const activeTasks = await getActiveTasksFromMES( @@ -328,6 +330,9 @@ export class XeticsInterfaceClient this._uri, this._token ); + + console.log(this.identifier, "got active tasks", activeTasks); + if (activeTasks.length > 0) { // If there are multiple Tasks. this.currentTask.setContent(activeTasks[0]); diff --git a/resources/admin-shell/instance/InstanceDetails.tsx b/resources/admin-shell/instance/InstanceDetails.tsx index 7bee922..339a1d5 100644 --- a/resources/admin-shell/instance/InstanceDetails.tsx +++ b/resources/admin-shell/instance/InstanceDetails.tsx @@ -10,6 +10,8 @@ import React from "react"; import { Card, ListGroup, Table } from "react-bootstrap"; import { ENopeDispatcherStatus } from "../../../lib/types/nope/nopeDispatcher.interface"; import { INopeModuleDescription } from "../../../lib/types/nope/nopeModule.interface"; +import { CustomIFrame } from "../../ui/iframe"; +import { StaticPopup } from "../../ui/popup/component"; import { StatusBadgeComponent } from "../generic/StatusBadge"; type ValidColors = "danger" | "warning" | "info" | "success"; @@ -31,6 +33,11 @@ export class InstanceDetailsComponent extends React.Component< }, { renderDetails: boolean; + customUiProps: { + name: string; + description: string; + link: string; + } | null; variant: ValidColors; } > { @@ -63,7 +70,8 @@ export class InstanceDetailsComponent extends React.Component< this.state = { renderDetails: this.props.renderDetails, - variant: dict[this.props.description.status] + variant: dict[this.props.description.status], + customUiProps: null }; } @@ -76,13 +84,17 @@ export class InstanceDetailsComponent extends React.Component< }); } + shouldComponentUpdate() { + return this.state.customUiProps === null; + } + render(): JSX.Element { const renderMethods = Object.getOwnPropertyNames(this.props.description.functions).length > 0; const renderProperties = Object.getOwnPropertyNames(this.props.description.properties).length > 0; - return ( + const elements = [ @@ -112,8 +124,17 @@ export class InstanceDetailsComponent extends React.Component< UI - {this.props.description.uiLinks?.map((link, idx) => ( - {link} + {this.props.description.uiLinks?.map((data, idx) => ( + { + this.setState({ + customUiProps: data + }); + }} + > + {data.name} + ))} @@ -174,6 +195,47 @@ export class InstanceDetailsComponent extends React.Component< - ); + ]; + + if (this.state.customUiProps) { + elements.push( + { + return ( + + ); + }, + props: { + link: this.state.customUiProps.link + } + }} + onHide={() => { + this.setState({ customUiProps: null }); + }} + header={ + "Visualization for " + + this.props.description.identifier + + " - " + + this.state.customUiProps.name + } + closeButton={true} + withoutBorder={false} + footer={ + this.props.description.author.forename + + " " + + this.props.description.author.surename + + " is responsible for the content. Report Bugs to " + + this.props.description.author.mail + } + size="xl" + > + ); + } + + return React.createElement("div", {}, ...elements); } } diff --git a/resources/ui/iframe.tsx b/resources/ui/iframe.tsx new file mode 100644 index 0000000..0d5d3cb --- /dev/null +++ b/resources/ui/iframe.tsx @@ -0,0 +1,80 @@ +/** + * @author Martin Karkowski + * @email m.karkowski@zema.de + * @create date 2021-04-20 18:51:42 + * @modify date 2021-04-20 18:55:10 + * @desc [description] + */ + +import React from "react"; + +/** + * A Component, used to Render the Staus of a Host. + * + */ +export class CustomIFrame extends React.Component< + { + height?: number; + width?: number; + link: string; + altText?: string; + }, + { + height: number; + width: number; + } +> { + ref: React.RefObject; + + /** + * Create the Status to Render. + * @param props + */ + constructor(props) { + super(props); + this.ref = React.createRef(); + this.state = { + height: 200, + width: 500 + }; + } + + protected _handleResize: () => void; + + componentDidMount() { + const _this = this; + this._handleResize = () => { + setTimeout(() => { + const size = { + width: + _this.props.width | + (_this.ref.current.parentElement.clientWidth - 40), + height: + _this.props.height | + (_this.ref.current.parentElement.clientHeight - 40) + }; + console.log(size, _this.ref); + _this.setState(size); + }, 200); + }; + + this._handleResize(); + window.addEventListener("resize", this._handleResize); + } + + componentWillUnmount() { + window.removeEventListener("resize", this._handleResize); + } + + render(): JSX.Element { + return ( +
+ +
+ ); + } +} diff --git a/resources/ui/layout/interfaces/IModalSettings.ts b/resources/ui/layout/interfaces/IModalSettings.ts index 86f2476..8aa5b2d 100644 --- a/resources/ui/layout/interfaces/IModalSettings.ts +++ b/resources/ui/layout/interfaces/IModalSettings.ts @@ -1,13 +1,13 @@ -import { IDynamicRenderSettings } from '../../dynamic/interfaces/IDynamicRenderSettings'; +import { IDynamicRenderSettings } from "../../dynamic/interfaces/IDynamicRenderSettings"; export interface IModalSettings { - content: IDynamicRenderSettings; - backdrop?: 'staic' | boolean - centered?: boolean - header?: string; - size?: 'sm' | 'lg' | 'xl', - closeButton?: boolean, - closeLabel?: boolean, - onHide?: (closeCallback: () => void) => void; - renderInBody?: boolean, -} \ No newline at end of file + content: IDynamicRenderSettings; + backdrop?: "static" | boolean; + centered?: boolean; + header?: string; + size?: "sm" | "lg" | "xl"; + closeButton?: boolean; + closeLabel?: boolean; + onHide?: (closeCallback: () => void) => void; + renderInBody?: boolean; +} diff --git a/resources/ui/popup/component.tsx b/resources/ui/popup/component.tsx new file mode 100644 index 0000000..bfe94aa --- /dev/null +++ b/resources/ui/popup/component.tsx @@ -0,0 +1,72 @@ +/** + * @author Martin Karkowski + * @email m.karkowski@zema.de + * @create date 2021-04-20 19:04:57 + * @modify date 2021-04-20 19:04:57 + * @desc [description] + */ + +import React from "react"; +import { Modal } from "react-bootstrap"; +import DynamicRenderer from "../dynamic/dynamicRenderer"; +import { IModalSettings } from "./interfaces/IModalSettings"; + +export class StaticPopup extends React.Component< + { withoutBorder?: boolean } & IModalSettings +> { + /** + * Create the Status to Render. + * @param props + */ + constructor(props) { + super(props); + this.state = { modalVisible: true }; + } + + render(): JSX.Element { + return ( + + {this.props.header ? ( + + {this.props.header} + + ) : ( + "" + )} + + {this.props.withoutBorder ? ( + + ) : ( + + {/* Render the dynamic Component */} + + + )} + + {this.props.footer ? ( + {this.props.footer} + ) : ( + "" + )} + + ); + } +} diff --git a/resources/ui/popup/interfaces/IModalSettings.ts b/resources/ui/popup/interfaces/IModalSettings.ts new file mode 100644 index 0000000..5b59ce2 --- /dev/null +++ b/resources/ui/popup/interfaces/IModalSettings.ts @@ -0,0 +1,26 @@ +/** + * @author Martin Karkowski + * @email m.karkowski@zema.de + * @create date 2021-04-20 19:06:22 + * @modify date 2021-04-20 19:06:28 + * @desc [description] + */ + +import { IDynamicRenderSettings } from "../../dynamic/interfaces/IDynamicRenderSettings"; + +export interface IModalSettings { + content: IDynamicRenderSettings; + backdrop?: "static" | boolean; + centered?: boolean; + header?: string; + size?: "sm" | "lg" | "xl"; + closeButton?: boolean; + closeLabel?: boolean; + onHide?: (closeCallback: () => void) => void; + renderInBody?: boolean; + footer?: + | string + | React.Component + | React.FunctionComponent + | ((...args) => JSX.Element); +}