From 21e4a946cd24b8a03ea577352f0271ebf7669ffa Mon Sep 17 00:00:00 2001 From: Michael DÜrre Date: Thu, 8 Apr 2021 07:27:18 +0200 Subject: update odlux for notification change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update due new notification protocol Issue-ID: CCSDK-3253 Signed-off-by: Michael DÜrre Change-Id: Iad65459fdc18603cd1ddbd97bb2211308744bd8b --- sdnr/wt/odlux/framework/src/app.tsx | 8 +- .../src/components/material-table/index.tsx | 44 +++++- .../src/components/material-table/tableToolbar.tsx | 4 +- .../src/components/material-table/utilities.ts | 64 +++++---- .../src/handlers/applicationStateHandler.ts | 2 +- .../framework/src/services/notificationService.ts | 156 +++++++++++---------- .../wt/odlux/framework/src/services/restService.ts | 42 ++++++ sdnr/wt/odlux/framework/src/views/about.tsx | 67 ++++++++- 8 files changed, 261 insertions(+), 126 deletions(-) (limited to 'sdnr/wt/odlux/framework/src') diff --git a/sdnr/wt/odlux/framework/src/app.tsx b/sdnr/wt/odlux/framework/src/app.tsx index 23ae2fbc9..2d913be1b 100644 --- a/sdnr/wt/odlux/framework/src/app.tsx +++ b/sdnr/wt/odlux/framework/src/app.tsx @@ -68,6 +68,10 @@ export const runApplication = () => { const initialToken = localStorage.getItem("userToken"); const applicationStore = applicationStoreCreator(); + if (initialToken) { + applicationStore.dispatch(new UpdateUser(User.fromString(initialToken) || undefined)); + } + window.onerror = function (msg: string, url: string, line: number, col: number, error: Error) { // Note that col & error are new to the HTML 5 spec and may not be // supported in every browser. It worked for me in Chrome. @@ -98,9 +102,7 @@ export const runApplication = () => { ReactDOM.render(, document.getElementById('app')); - if (initialToken) { - applicationStore.dispatch(new UpdateUser(User.fromString(initialToken) || undefined)); - } + }; diff --git a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx index 7d4633bc6..c74fd1a38 100644 --- a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx +++ b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx @@ -32,13 +32,14 @@ import { EnhancedTableHead } from './tableHead'; import { EnhancedTableFilter } from './tableFilter'; import { ColumnModel, ColumnType } from './columnModel'; -import { Omit, Menu } from '@material-ui/core'; +import { Omit, Menu, makeStyles } from '@material-ui/core'; import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; import { DividerTypeMap } from '@material-ui/core/Divider'; import { MenuItemProps } from '@material-ui/core/MenuItem'; import { flexbox } from '@material-ui/system'; +import { RowDisabled } from './utilities'; export { ColumnModel, ColumnType } from './columnModel'; type propType = string | number | null | undefined | (string | number)[]; @@ -103,6 +104,34 @@ const styles = (theme: Theme) => createStyles({ } }); +const useTableRowExtStyles = makeStyles((theme: Theme) => createStyles({ + disabled: { + color: "rgba(180, 180, 180, 0.7)", + }, +})); + +type GetStatelessComponentProps = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any; +type TableRowExtProps = GetStatelessComponentProps & { disabled: boolean }; +const TableRowExt : React.FC = (props) => { + const [disabled, setDisabled] = React.useState(true); + const classes = useTableRowExtStyles(); + + const onMouseDown = (ev: React.MouseEvent) => { + if (ev.button ===1){ + setDisabled(!disabled); + ev.preventDefault(); + ev.stopPropagation(); + } else if (props.disabled && disabled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + + return ( + + ); +}; + export type MaterialTableComponentState = { order: 'asc' | 'desc'; orderBy: string | null; @@ -130,7 +159,7 @@ type MaterialTableComponentBaseProps = WithStyles & { enableSelection?: boolean; disableSorting?: boolean; disableFilter?: boolean; - customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void, disabled?: boolean }[]; onHandleClick?(event: React.MouseEvent, rowData: TData): void; createContextMenu?: (row: TData) => React.ReactElement, React.ComponentType>>[]; }; @@ -222,12 +251,12 @@ class MaterialTableComponent extends React.Component {showFilter && || null} {rows // may need ordering here - .map((entry: TData & { [key: string]: any }, index) => { + .map((entry: TData & { [RowDisabled]?: boolean, [kex: string]: any }, index) => { const entryId = getId(entry); const isSelected = this.isSelected(entryId); const contextMenu = (this.props.createContextMenu && this.state.contextMenuInfo.index === index && this.props.createContextMenu(entry)) || null; return ( - { if (this.props.createContextMenu) { @@ -252,9 +281,10 @@ class MaterialTableComponent extends React.Component {this.props.enableSelection - ? + ? : null @@ -264,7 +294,7 @@ class MaterialTableComponent extends React.Component { const style = col.width ? { width: col.width } : {}; return ( - + {col.type === ColumnType.custom && col.customControl ? : col.type === ColumnType.boolean @@ -280,7 +310,7 @@ class MaterialTableComponent extends React.Component {contextMenu} || null} - + ); })} {emptyRows > 0 && ( diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx index 3b2f8e0a8..f7de0a062 100644 --- a/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx @@ -67,7 +67,7 @@ interface ITableToolbarComponentProps extends WithStyles { numSelected: number | null; title?: string; tableId?: string; - customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void, disabled?: boolean }[]; onToggleFilter: () => void; onExportToCsv: () => void; } @@ -110,7 +110,7 @@ class TableToolbarComponent extends React.Component ( - action.onClick()}> + action.onClick()}> diff --git a/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts index 07ffe2ff5..544e14e01 100644 --- a/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts +++ b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts @@ -21,12 +21,14 @@ import { Dispatch } from '../../flux/store'; import { AddErrorInfoAction } from '../../actions/errorActions'; import { IApplicationStoreState } from '../../store/applicationStore'; +export const RowDisabled = Symbol("RowDisabled"); import { DataCallback } from "."; + export interface IExternalTableState { order: 'asc' | 'desc'; orderBy: string | null; selected: any[] | null; - rows: TData[]; + rows: (TData & { [RowDisabled]?: boolean })[]; total: number; page: number; rowsPerPage: number; @@ -36,8 +38,31 @@ export interface IExternalTableState { preFilter: { [property: string]: string }; } +export type ExternalMethodes = { + reloadAction: (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => Promise; + createActions: (dispatch: Dispatch, skipRefresh?: boolean) => { + onRefresh: () => void; + onHandleRequestSort: (orderBy: string) => void; + onHandleExplicitRequestSort: (property: string, sortOrder: "asc" | "desc") => void; + onToggleFilter: (refresh?: boolean | undefined) => void; + onFilterChanged: (property: string, filterTerm: string) => void; + onHandleChangePage: (page: number) => void; + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void; + }; + createPreActions: (dispatch: Dispatch, skipRefresh?: boolean) => { + onPreFilterChanged: (preFilter: { + [key: string]: string; + }) => void; + }; + createProperties: (state: IApplicationStoreState) => IExternalTableState; + actionHandler: IActionHandler, Action>; +} + + /** Create an actionHandler and actions for external table states. */ -export function createExternal(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState) { +export function createExternal(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState) : ExternalMethodes ; +export function createExternal(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState, disableRow: (data: TData) => boolean) : ExternalMethodes; +export function createExternal(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState, disableRow?: (data: TData) => boolean) : ExternalMethodes { //#region Actions abstract class TableAction extends Action { } @@ -131,7 +156,9 @@ export function createExternal(callback: DataCallback, selectState state = { ...state, loading: false, - rows: action.result.rows, + rows: disableRow + ? action.result.rows.map((row: TData) => ({...row, [RowDisabled]: disableRow(row) })) + : action.result.rows, total: action.result.total, page: action.result.page, } @@ -191,7 +218,7 @@ export function createExternal(callback: DataCallback, selectState dispatch(new RefreshAction()); const ownState = selectState(getAppState()); const filter = { ...ownState.preFilter, ...(ownState.showFilter && ownState.filter || {}) }; - Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)).then(result => { + return Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)).then(result => { if (ownState.page > 0 && ownState.rowsPerPage * ownState.page > result.total) { //if result is smaller than the currently shown page, new search and repaginate @@ -207,30 +234,7 @@ export function createExternal(callback: DataCallback, selectState } - }).catch(error => new AddErrorInfoAction(error)); - }; - - const reloadActionAsync = async (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { - dispatch(new RefreshAction()); - const ownState = selectState(getAppState()); - const filter = { ...ownState.preFilter, ...(ownState.showFilter && ownState.filter || {}) }; - - try { - const result = await Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)); - - - if (ownState.page > 0 && ownState.rowsPerPage * ownState.page > result.total) { //if result is smaller than the currently shown page, new search and repaginate - - let newPage = Math.floor(result.total / ownState.rowsPerPage); - - const repaginationResult = await Promise.resolve(callback(newPage, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)); - dispatch(new SetResultAction(repaginationResult)); - } else { - dispatch(new SetResultAction(result)); - } - } catch (error) { - new AddErrorInfoAction(error); - } + }).catch(error => dispatch(new AddErrorInfoAction(error))); }; const createPreActions = (dispatch: Dispatch, skipRefresh: boolean = false) => { @@ -303,6 +307,6 @@ export function createExternal(callback: DataCallback, selectState createProperties: createProperties, createPreActions: createPreActions, actionHandler: externalTableStateActionHandler, - reloadActionAsync: reloadActionAsync, } -} \ No newline at end of file +} + diff --git a/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts index b5c1ee7b1..06df6709f 100644 --- a/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts +++ b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts @@ -58,7 +58,7 @@ const applicationStateInit: IApplicationState = { export const configureApplication = (config: ApplicationConfig) => { applicationStateInit.authentication = config.authentication === "oauth" ? "oauth" : "basic"; - applicationStateInit.enablePolicy = config.authentication ? true : false; + applicationStateInit.enablePolicy = config.enablePolicy ? true : false; } export const applicationStateHandler: IActionHandler = (state = applicationStateInit, action) => { diff --git a/sdnr/wt/odlux/framework/src/services/notificationService.ts b/sdnr/wt/odlux/framework/src/services/notificationService.ts index 30091b574..5625b1f55 100644 --- a/sdnr/wt/odlux/framework/src/services/notificationService.ts +++ b/sdnr/wt/odlux/framework/src/services/notificationService.ts @@ -15,7 +15,6 @@ * the License. * ============LICENSE_END========================================================================== */ -import * as X2JS from 'x2js'; import { ApplicationStore } from '../store/applicationStore'; import { SetWebsocketAction } from '../actions/websocketAction'; @@ -26,81 +25,95 @@ let userLoggedOut = false; let wasWebsocketConnectionEstablished: undefined | boolean; let applicationStore: ApplicationStore | null; - export interface IFormatedMessage { - notifType: string | null; - time: string; + "event-time": string, + "data": { + "counter": number, + "attribute-name": string, + "time-stamp": string, + "object-id-ref": string, + "new-value": string + }, + "node-id": string, + "type": { + "namespace": string, + "revision": string, + "type": string + } } export type SubscriptionCallback = (msg: TMessage) => void; -function formatData(event: MessageEvent): IFormatedMessage | undefined { - - var x2js = new X2JS(); - var jsonObj: { [key: string]: IFormatedMessage } = x2js.xml2js(event.data); - if (jsonObj && typeof (jsonObj) === 'object') { - - const notifType = Object.keys(jsonObj)[0]; - const formated = jsonObj[notifType]; - formated.notifType = notifType; - formated.time = new Date().toISOString(); - return formated; - } - return undefined; - +function setCurrentSubscriptions(notificationSocket: WebSocket) { + const scopesToSubscribe = Object.keys(subscriptions); + if (notificationSocket.readyState === notificationSocket.OPEN) { + const data = { + 'data': 'scopes', + 'scopes':[{ + "schema":{ + "namespace":"*", + "revision":"*", + "notification": scopesToSubscribe + } + }] + }; + notificationSocket.send(JSON.stringify(data)); + return true; + }; + return false; } -export function subscribe(scope: string | string[], callback: SubscriptionCallback): boolean { +function addScope(scope: string | string[], callback: SubscriptionCallback) { const scopes = scope instanceof Array ? scope : [scope]; - // send all new scopes to subscribe - const newScopesToSubscribe: string[] = scopes.reduce((acc: string[], cur: string) => { - const currentCallbacks = subscriptions[cur]; - if (currentCallbacks) { - if (!currentCallbacks.some(c => c === callback)) { - currentCallbacks.push(callback); + // send all new scopes to subscribe + const newScopesToSubscribe: string[] = scopes.reduce((acc: string[], cur: string) => { + const currentCallbacks = subscriptions[cur]; + if (currentCallbacks) { + if (!currentCallbacks.some(c => c === callback)) { + currentCallbacks.push(callback); + } + } else { + subscriptions[cur] = [callback]; + acc.push(cur); } - } else { - subscriptions[cur] = [callback]; - acc.push(cur); - } - return acc; - }, []); + return acc; + }, []); - if (newScopesToSubscribe.length === 0) { - return true; - } + if (newScopesToSubscribe.length === 0) { + return true; + } + return false; +} - return true; +function removeScope(scope: string | string[], callback: SubscriptionCallback) { + const scopes = scope instanceof Array ? scope : [scope]; + scopes.forEach(s => { + const callbacks = subscriptions[s]; + const index = callbacks && callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + if (callbacks.length === 0) { + subscriptions[s] === undefined; + } + }); } +export function subscribe(scope: string | string[], callback: SubscriptionCallback): Promise { + addScope(scope, callback) + return socketReady && socketReady.then((notificationSocket) => { + // send a subscription to all active scopes + return setCurrentSubscriptions(notificationSocket); + }) || true; +} export function unsubscribe(scope: string | string[], callback: SubscriptionCallback): Promise { - return socketReady.then((notificationSocket) => { - const scopes = scope instanceof Array ? scope : [scope]; - scopes.forEach(s => { - const callbacks = subscriptions[s]; - const index = callbacks && callbacks.indexOf(callback); - if (index > -1) { - callbacks.splice(index, 1); - } - if (callbacks.length === 0) { - subscriptions[s] === undefined; - } - }); - + removeScope(scope, callback); + return socketReady && socketReady.then((notificationSocket) => { // send a subscription to all active scopes - const scopesToSubscribe = Object.keys(subscriptions); - if (notificationSocket.readyState === notificationSocket.OPEN) { - const data = { - 'data': 'scopes', - 'scopes': scopesToSubscribe - }; - notificationSocket.send(JSON.stringify(data)); - return true; - } - return false; - }); + return setCurrentSubscriptions(notificationSocket); + }) || true; } export const startNotificationService = (store: ApplicationStore) => { @@ -111,24 +124,24 @@ const connect = (): Promise => { return new Promise((resolve, reject) => { const notificationSocket = new WebSocket(socketUrl); - notificationSocket.onmessage = (event) => { + notificationSocket.onmessage = (event: MessageEvent) => { // process received event - if (typeof event.data === 'string') { - const formated = formatData(event); - if (formated && formated.notifType) { - const callbacks = subscriptions[formated.notifType]; + + if (event.data && typeof event.data === "string" ) { + const msg = JSON.parse(event.data) as IFormatedMessage; + const callbacks = msg?.type?.type && subscriptions[msg.type.type]; if (callbacks) { callbacks.forEach(cb => { // ensure all callbacks will be called try { - return cb(formated); + return cb(msg); } catch (reason) { console.error(reason); } }); } } - } + }; notificationSocket.onerror = function (error) { @@ -148,14 +161,7 @@ const connect = (): Promise => { resolve(notificationSocket); // send a subscription to all active scopes - const scopesToSubscribe = Object.keys(subscriptions); - if (notificationSocket.readyState === notificationSocket.OPEN) { - const data = { - 'data': 'scopes', - 'scopes': scopesToSubscribe - }; - notificationSocket.send(JSON.stringify(data)); - }; + setCurrentSubscriptions(notificationSocket); }; notificationSocket.onclose = function (event) { @@ -171,8 +177,6 @@ const connect = (): Promise => { } - - export const startWebsocketSession = () => { socketReady = connect(); userLoggedOut = false; diff --git a/sdnr/wt/odlux/framework/src/services/restService.ts b/sdnr/wt/odlux/framework/src/services/restService.ts index f05c7b89f..c7b122449 100644 --- a/sdnr/wt/odlux/framework/src/services/restService.ts +++ b/sdnr/wt/odlux/framework/src/services/restService.ts @@ -15,6 +15,8 @@ * the License. * ============LICENSE_END========================================================================== */ + + import { ApplicationStore } from "../store/applicationStore"; import { ReplaceAction } from "../actions/navigationActions"; @@ -30,6 +32,46 @@ export const formEncode = (params: { [key: string]: string | number }) => Object return encodeURIComponent(key) + '=' + encodeURIComponent(params[key].toString()); }).join('&'); +const wildcardToRegexp = (pattern: string) => { + return new RegExp('^' + pattern.split(/\*\*/).map((p) => p.split(/\*+/).map((i) => i.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')).join('^[/]')).join('.*') + '$'); +}; + +export const getAccessPolicyByUrl = (url: string) => { + const result = { + GET : false, + POST: false, + PUT: false, + PATCH: false, + DELETE: false, + }; + + if (!applicationStore) return result; + + const { state: { framework: { applicationState: { enablePolicy }, authenticationState: { policies }}} } = applicationStore!; + + result.GET = true; + result.POST = true; + result.PUT = true; + result.PATCH = true; + result.DELETE = true; + + if (!enablePolicy || !policies || policies.length === 0) return result; + + policies.forEach(p => { + const re = wildcardToRegexp(p.path); + if (re.test(url)) { + result.GET = p.methods.get != null ? p.methods.get : result.GET ; + result.POST = p.methods.post != null ? p.methods.post : result.POST ; + result.PUT = p.methods.put != null ? p.methods.put : result.PUT ; + result.PATCH = p.methods.patch != null ? p.methods.patch : result.PATCH ; + result.DELETE = p.methods.delete != null ? p.methods.delete : result.DELETE ; + } + }); + + return result; + +} + /** Sends a rest request to the given path. * @returns The data, or null it there was any error */ diff --git a/sdnr/wt/odlux/framework/src/views/about.tsx b/sdnr/wt/odlux/framework/src/views/about.tsx index f97d6ffb3..5d2257a3f 100644 --- a/sdnr/wt/odlux/framework/src/views/about.tsx +++ b/sdnr/wt/odlux/framework/src/views/about.tsx @@ -20,6 +20,7 @@ import * as marked from 'marked'; import * as hljs from 'highlight.js'; import { requestRestExt } from '../services/restService'; import { Button, Typography } from '@material-ui/core'; +import createBreakpoints from '@material-ui/core/styles/createBreakpoints'; const defaultRenderer = new marked.Renderer(); defaultRenderer.link = (href, title, text) => ( `${text}` @@ -30,6 +31,23 @@ interface AboutState { isContentLoadedSucessfully: boolean; } +type odluxVersion= {version:string,build:string, framework: string, + applications:{ + configurationApp: string, + connectApp: string, + eventLogApp: string, + faultApp: string, + helpApp: string, + inventoryApp: string, + linkCalculationApp: string, + maintenanceApp: string, + mediatorApp: string, + networkMapApp: string, + permanceHistoryApp: string + }}; + +type topologyVersion = {version: string}; + class AboutComponent extends React.Component { textarea: React.RefObject; @@ -40,23 +58,58 @@ class AboutComponent extends React.Component { this.textarea = React.createRef(); this.loadAboutContent(); } - private getMarkOdluxVersionMarkdownTable(data:{version:string,build:string}|null|undefined):string{ + + private getMarkOdluxVersionMarkdownTable(data:odluxVersion|null|undefined):string{ if(!data) { return ""; + }else{ + let applicationVersions= ''; + if(data.applications){ + + applicationVersions = `| Framework | ${data.framework}|\n `+ + `| ConnectApp | ${data.applications.connectApp}|\n `+ + `| FaultApp | ${data.applications.faultApp}|\n `+ + `| MaintenanceApp | ${data.applications.maintenanceApp}|\n `+ + `| ConfigurationApp | ${data.applications.configurationApp}|\n `+ + `| PerformanceHistoryApp | ${data.applications.permanceHistoryApp}|\n `+ + `| InventoryApp | ${data.applications.inventoryApp}|\n `+ + `| EventLogApp | ${data.applications.eventLogApp}|\n `+ + `| MediatorApp | ${data.applications.mediatorApp}|\n `+ + `| NetworkMapApp | ${data.applications.networkMapApp}|\n `+ + `| LinkCalculatorApp | ${data.applications.linkCalculationApp}|\n `+ + `| HelpApp | ${data.applications.helpApp}|\n `; + } + + return `| | |\n| --- | --- |\n| Version | ${data.version} |\n| Build timestamp | ${data.build}|\n`+ + applicationVersions; } - return `| | |\n| --- | --- |\n| Version | ${data.version} |\n| Build timestamp | ${data.build}|` } + + private getTopologyVersionMarkdownTable(data: topologyVersion|null|undefined){ + if(!data){ + return "No version"; + } + else + { + return `| | |\n| --- | --- |\n| Version | ${data.version} |\n` + } + } + private loadAboutContent(): void { const baseUri = window.location.pathname.substring(0,window.location.pathname.lastIndexOf("/")+1); const p1 = requestRestExt('/about'); - const p2 = requestRestExt<{version:string,build:string}>(`${baseUri}version.json`); - Promise.all([p1,p2]).then((responses) => { + const p2 = requestRestExt(`${baseUri}version.json`); + const p3 = requestRestExt(`/topology/info/version`); + + Promise.all([p1,p2, p3]).then((responses) => { const response = responses[0]; - const response2 = responses[1]; + const response2 = responses[1]; + const response3 = responses[2]; const content = response.status == 200 ? response.data : `${response.status} ${response.message}` || "Server error"; - const content2 = `\n## ODLUX Version Info\n`+(response2.status == 200 ? this.getMarkOdluxVersionMarkdownTable(response2.data) : `${response2.status} ${response2.message}` || "ODLUX Server error"); + const content2 = `\n## ODLUX Version Info\n`+(response2.status == 200 ? this.getMarkOdluxVersionMarkdownTable(response2.data) : `${response2.message}` || "ODLUX Server error"); + const content3 = `\n## Topology API Version Info\n`+(response3.status == 200 ? this.getTopologyVersionMarkdownTable(response3.data): `Topology API not available`); const loadedSucessfully = response.status == 200 ? true : false; - this.setState({ content: (content + content2) || null, isContentLoadedSucessfully: loadedSucessfully }); + this.setState({ content: (content + content2 + content3 ) || null, isContentLoadedSucessfully: loadedSucessfully }); }).catch((error) => { this.setState({ content: error }) }) -- cgit 1.2.3-korg