From fa45ee0202025a9bad5a83ef25c2276f4140b16f Mon Sep 17 00:00:00 2001 From: fpesek Date: Thu, 22 Sep 2022 18:05:28 +0200 Subject: [PATCH 01/33] WIP: websocket implementation trial --- .../MicroservicesContainer.js | 124 +++++++++++++----- 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index e36a31557..db9e629b6 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Container } from 'reactstrap'; @@ -16,7 +16,67 @@ export default (props) => { const { t } = useTranslation(); - const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); + + let wsSubPath = '/ws'; + // const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); + const serviceName = 'lmio_remote_control'; + let WSUrl = props.app.getWebSocketURL(serviceName, wsSubPath); + console.log(WSUrl, "WS URL") + let WSClient = null; + + const isMounted = useRef(null); + + + useEffect(() => { + isMounted.current = true; + + if (WSUrl != undefined) { + console.log('RECONNECT JEDE?') + reconnect(); + } + console.log(WSClient, "WS CLIENT") + return () => { + if (WSClient != null) { + try { + WSClient.close(); + } catch (e) { + console.log("Ignored exception: ", e) + } + } + + isMounted.current = false; + } + }, []); + + const reconnect = () => { + console.log(WSClient, 'WS CLIENT V RECONNECT') + if (WSClient != null) { + try { + WSClient.close(); + } catch (e) { + console.log("Ignored exception: ", e) + } + } + + if (isMounted.current === false) return; + + WSClient = props.app.createWebSocket(serviceName, wsSubPath); + + console.log(WSClient, "WS CLIENT V RECONNECT 2") + + WSClient.onmessage = (message) => { + // DO something + console.log(message, "MESSAGE") + }; + + WSClient.onerror = (error) => { + // DO something + console.warn(error, "ERROR Z WEBSOCKETU") + setTimeout(() => { + reconnect(); + }, 3000, this); + }; + } const headers = [ { @@ -39,39 +99,39 @@ export default (props) => { { name: t('MicroservicesContainer|Version'), key: 'version'} ]; - // Filter the value - const onSearch = (value) => { - setFilter(value); - }; + // // Filter the value + // const onSearch = (value) => { + // setFilter(value); + // }; - useEffect(() => { - getMicroservicesList(); - }, [page, filter ,limit]); + // useEffect(() => { + // getMicroservicesList(); + // }, [page, filter ,limit]); - const getMicroservicesList = async () => { - try { - const response = await LMIORemoteControlAPI.get('/microservices', { params: { p: page, i: limit, f: filter }}); + // const getMicroservicesList = async () => { + // try { + // const response = await LMIORemoteControlAPI.get('/microservices', { params: { p: page, i: limit, f: filter }}); - if (response.data.result !== "OK") throw new Error(response); + // if (response.data.result !== "OK") throw new Error(response); - setList(response.data.data); - setCount(response.data.count); - } catch (e) { - console.error(e); - props.app.addAlert("warning", t('MicroservicesContainer|Failed to fetch data')); - } - } + // setList(response.data.data); + // setCount(response.data.count); + // } catch (e) { + // console.error(e); + // props.app.addAlert("warning", t('MicroservicesContainer|Failed to fetch data')); + // } + // } - const customRowStyle = { - style: { - backgroundColor: "#fff3cd", - color: "#856404" - }, - condition: (obj) => { - if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) return true; - return false; - } - } + // const customRowStyle = { + // style: { + // backgroundColor: "#fff3cd", + // color: "#856404" + // }, + // condition: (obj) => { + // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) return true; + // return false; + // } + // } return ( @@ -85,11 +145,11 @@ export default (props) => { setLimit={setLimit} limitValues={[20, 50, 100]} search={{ icon: 'cil-magnifying-glass', placeholder: t("CredentialsListContainer|Search") }} - onSearch={onSearch} + // onSearch={onSearch} title={{ text: t('MicroservicesContainer|Microservices'), icon: "cil-list" }} - customRowStyle={customRowStyle} + // customRowStyle={customRowStyle} /> ) From 9c0203e21a557ca2da34717b2580fa66fcd5cdc6 Mon Sep 17 00:00:00 2001 From: fpesek Date: Fri, 23 Sep 2022 16:53:43 +0200 Subject: [PATCH 02/33] Obtain data and render it in ReactJson --- .../MicroservicesContainer.js | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index db9e629b6..a4f184894 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import ReactJson from 'react-json-view'; import { Container } from 'reactstrap'; @@ -8,11 +9,13 @@ import { DataTable } from 'asab-webui'; import "./microservices.scss"; export default (props) => { - const [list, setList] = useState([]); - const [page, setPage] = useState(1); - const [count, setCount] = useState(0); - const [filter, setFilter] = useState(""); - const [limit, setLimit] = useState(20); + // const [list, setList] = useState([]); + // const [page, setPage] = useState(1); + // const [count, setCount] = useState(0); + // const [filter, setFilter] = useState(""); + // const [limit, setLimit] = useState(20); + + const [data, setData] = useState({}); const { t } = useTranslation(); @@ -21,7 +24,6 @@ export default (props) => { // const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); const serviceName = 'lmio_remote_control'; let WSUrl = props.app.getWebSocketURL(serviceName, wsSubPath); - console.log(WSUrl, "WS URL") let WSClient = null; const isMounted = useRef(null); @@ -31,10 +33,9 @@ export default (props) => { isMounted.current = true; if (WSUrl != undefined) { - console.log('RECONNECT JEDE?') reconnect(); } - console.log(WSClient, "WS CLIENT") + return () => { if (WSClient != null) { try { @@ -62,11 +63,21 @@ export default (props) => { WSClient = props.app.createWebSocket(serviceName, wsSubPath); - console.log(WSClient, "WS CLIENT V RECONNECT 2") + // TODO: remove onopen + WSClient.onopen = () => { + console.log('ws connection open'); + } WSClient.onmessage = (message) => { + if (IsJsonString(message.data) == true) { + setData(JSON.parse(message.data)); + } else { + const err = {}; + err["parsingError"] = true; + setData(err); + } // DO something - console.log(message, "MESSAGE") + console.log(JSON.parse(message.data), "MESSAGE") }; WSClient.onerror = (error) => { @@ -135,22 +146,22 @@ export default (props) => { return ( - ) } + +// Check if string is valid JSON +function IsJsonString(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +} From ef561244c4a0dfbf3d19c55436636ed23087bdc8 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 26 Sep 2022 10:01:59 +0200 Subject: [PATCH 03/33] Add spinner when wainting for data from ws --- .../MicroservicesContainer.js | 60 ++++++++++--------- .../microservices.scss | 4 ++ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index a4f184894..5c1f3eac4 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -4,7 +4,7 @@ import ReactJson from 'react-json-view'; import { Container } from 'reactstrap'; -import { DataTable } from 'asab-webui'; +import { DataTable, Spinner } from 'asab-webui'; import "./microservices.scss"; @@ -16,6 +16,7 @@ export default (props) => { // const [limit, setLimit] = useState(20); const [data, setData] = useState({}); + const [loading, setLoading] = useState(true); const { t } = useTranslation(); @@ -65,10 +66,12 @@ export default (props) => { // TODO: remove onopen WSClient.onopen = () => { + // setLoading(false); console.log('ws connection open'); } WSClient.onmessage = (message) => { + setLoading(false); if (IsJsonString(message.data) == true) { setData(JSON.parse(message.data)); } else { @@ -82,6 +85,7 @@ export default (props) => { WSClient.onerror = (error) => { // DO something + setLoading(false); console.warn(error, "ERROR Z WEBSOCKETU") setTimeout(() => { reconnect(); @@ -89,26 +93,26 @@ export default (props) => { }; } - const headers = [ - { - name: " ", - customHeaderStyle: { width: '2.5rem' }, - customComponent: { - generate: (obj) => { - if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) { - return ( - - ) - } - } - } - }, - { name: t('MicroservicesContainer|ID'), key: 'id', link: { key: "id", pathname: "/microservices/svcs/" } }, - { name: t('MicroservicesContainer|Host'), key: 'hostname' }, - { name: t('MicroservicesContainer|Launch time'), key: 'launchtime', datetime: true }, - { name: t('MicroservicesContainer|Created at'), key: 'created_at', datetime: true }, - { name: t('MicroservicesContainer|Version'), key: 'version'} - ]; + // const headers = [ + // { + // name: " ", + // customHeaderStyle: { width: '2.5rem' }, + // customComponent: { + // generate: (obj) => { + // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) { + // return ( + // + // ) + // } + // } + // } + // }, + // { name: t('MicroservicesContainer|ID'), key: 'id', link: { key: "id", pathname: "/microservices/svcs/" } }, + // { name: t('MicroservicesContainer|Host'), key: 'hostname' }, + // { name: t('MicroservicesContainer|Launch time'), key: 'launchtime', datetime: true }, + // { name: t('MicroservicesContainer|Created at'), key: 'created_at', datetime: true }, + // { name: t('MicroservicesContainer|Version'), key: 'version'} + // ]; // // Filter the value // const onSearch = (value) => { @@ -146,12 +150,14 @@ export default (props) => { return ( - + {loading == true ?
: + + }
) } diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss index d19f70ffc..15799ee7d 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss @@ -3,3 +3,7 @@ font-family: monospace; } } + +.spinner { + padding-top: 10rem; +} \ No newline at end of file From debfea8ed3f1f11e7b0581097d6753c3f01e6a0e Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 26 Sep 2022 12:24:27 +0200 Subject: [PATCH 04/33] Implement parser error, rendering data by its key --- .../MicroservicesContainer.js | 54 +++++++++++++++---- .../maintenance/MicroservicesModule/index.js | 2 + 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index 5c1f3eac4..b094f0284 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -1,13 +1,12 @@ import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import ReactJson from 'react-json-view'; +import { useSelector } from 'react-redux'; -import { Container } from 'reactstrap'; +import { Container, Card, CardBody } from 'reactstrap'; import { DataTable, Spinner } from 'asab-webui'; -import "./microservices.scss"; - export default (props) => { // const [list, setList] = useState([]); // const [page, setPage] = useState(1); @@ -17,6 +16,12 @@ export default (props) => { const [data, setData] = useState({}); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + const emptyContentImg = props.app.Config.get('brand_image').full; + const emptyContentAlt = props.app.Config.get('title'); + + const theme = useSelector(state => state.theme); const { t } = useTranslation(); @@ -79,14 +84,14 @@ export default (props) => { err["parsingError"] = true; setData(err); } + setError(false); // DO something console.log(JSON.parse(message.data), "MESSAGE") }; WSClient.onerror = (error) => { - // DO something setLoading(false); - console.warn(error, "ERROR Z WEBSOCKETU") + setError(true); setTimeout(() => { reconnect(); }, 3000, this); @@ -151,12 +156,39 @@ export default (props) => { return ( {loading == true ?
: - + error == true ? +
+ + + {emptyContentAlt} +

{t("MicroservicesContainer|Can't establish websocket connection, data can't be loaded")}

+
+
+
+ : + (data["parsingError"] == true) ? +
+
{t("MicroservicesContainer|Can't display data due to parsing error")}
+
+ : + + + {data && Object.keys(data).map((key, idx) => { +
+ {key}: +
+ })} +
+
}
) diff --git a/src/modules/maintenance/MicroservicesModule/index.js b/src/modules/maintenance/MicroservicesModule/index.js index 95f1e9e5d..dcdfb28e9 100644 --- a/src/modules/maintenance/MicroservicesModule/index.js +++ b/src/modules/maintenance/MicroservicesModule/index.js @@ -3,6 +3,8 @@ import Module from 'asab-webui/abc/Module'; import MicroservicesContainer from "./MicroservicesContainers/MicroservicesContainer"; import MicroserviceDetailContainer from "./MicroservicesContainers/MicroserviceDetailContainer"; +import "./MicroservicesContainers/microservices.scss"; + export default class MicroservicesModule extends Module { constructor(app, name) { super(app, "ASABMicroservicesModule"); From 2a1d03e8851af129b8fea48091462825fb645c12 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 26 Sep 2022 17:59:00 +0200 Subject: [PATCH 05/33] Make container height 100% --- .../MicroservicesContainers/microservices.scss | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss index 15799ee7d..9b8f31689 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss @@ -1,9 +1,10 @@ +@import "~asab-webui/styles/constants/index.scss"; + .svcs-container { - td:nth-child(3n) { - font-family: monospace; - } + height: 100%; + } -.spinner { - padding-top: 10rem; -} \ No newline at end of file +.microservices-body { + overflow: auto; +} From 69a953c3a0faf0a68440fdf7d8da160ca85474a9 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 26 Sep 2022 17:59:38 +0200 Subject: [PATCH 06/33] Render full frame and delta frame data with useMemo --- .../MicroservicesContainer.js | 174 ++++++++++++------ 1 file changed, 120 insertions(+), 54 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index b094f0284..321aebf3f 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -1,9 +1,9 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import ReactJson from 'react-json-view'; import { useSelector } from 'react-redux'; -import { Container, Card, CardBody } from 'reactstrap'; +import { Container, Card, CardBody, CardHeader } from 'reactstrap'; import { DataTable, Spinner } from 'asab-webui'; @@ -14,7 +14,9 @@ export default (props) => { // const [filter, setFilter] = useState(""); // const [limit, setLimit] = useState(20); - const [data, setData] = useState({}); + const [fullFrameData, setFullFrameData] = useState({}); + const [fFData, setFFData] = useState(false); + const [deltaFrameData, setDeltaFrameData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -55,8 +57,40 @@ export default (props) => { } }, []); + // Use memo for data rendering (due to expensive caluclations) + const data = useMemo(() => { + if (fFData == true) { + // Render full frame data + setFFData(false); + return fullFrameData; + } else { + // Render delta frame data + if(deltaFrameData && Object.keys(deltaFrameData)) { + // If key in full frame, update data, otherwise append deltaframe to fullframe object without mutating the original object + if (fullFrameData[Object.keys(deltaFrameData)[0]]) { + // New additions / updates to values of object + const additions = Object.values(deltaFrameData) ? Object.values(deltaFrameData)[0] : {}; + // Append new values / updates to values of object + let updateValues = {...fullFrameData[Object.keys(deltaFrameData)[0]], ...additions} + // Create a new key-value pair from deltaFrame key and new/updated values + let newObj = {}; + newObj[Object.keys(deltaFrameData)[0]] = updateValues; + // Append new object to fullFrame data object without mutation of original one + let renderAll = {...fullFrameData, ...newObj}; + // Return object + return renderAll; + } else { + // Append deltaFrame object to fullFrame object and return it + let renderAll = {...fullFrameData, ...deltaFrameData}; + return renderAll; + } + } + // Fallback if deltaFrameData will not meet the condition requirements + return fullFrameData; + } + }, [fullFrameData, deltaFrameData]) + const reconnect = () => { - console.log(WSClient, 'WS CLIENT V RECONNECT') if (WSClient != null) { try { WSClient.close(); @@ -71,22 +105,29 @@ export default (props) => { // TODO: remove onopen WSClient.onopen = () => { - // setLoading(false); console.log('ws connection open'); } WSClient.onmessage = (message) => { setLoading(false); if (IsJsonString(message.data) == true) { - setData(JSON.parse(message.data)); + let retrievedData = JSON.parse(message.data); + if (retrievedData && Object.keys(retrievedData)) { + if (Object.keys(retrievedData).length > 1) { + // Set full frame data + setFullFrameData(retrievedData); + setFFData(true); + } else { + // Set delta frame data + setDeltaFrameData(retrievedData); + } + } } else { const err = {}; err["parsingError"] = true; setData(err); } setError(false); - // DO something - console.log(JSON.parse(message.data), "MESSAGE") }; WSClient.onerror = (error) => { @@ -99,20 +140,20 @@ export default (props) => { } // const headers = [ - // { - // name: " ", - // customHeaderStyle: { width: '2.5rem' }, - // customComponent: { - // generate: (obj) => { - // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) { - // return ( - // - // ) - // } - // } - // } - // }, - // { name: t('MicroservicesContainer|ID'), key: 'id', link: { key: "id", pathname: "/microservices/svcs/" } }, + // // { + // // name: " ", + // // customHeaderStyle: { width: '2.5rem' }, + // // customComponent: { + // // generate: (obj) => { + // // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) { + // // return ( + // // + // // ) + // // } + // // } + // // } + // // }, + // { name: t('MicroservicesContainer|ID'), key: 'id' }, // { name: t('MicroservicesContainer|Host'), key: 'hostname' }, // { name: t('MicroservicesContainer|Launch time'), key: 'launchtime', datetime: true }, // { name: t('MicroservicesContainer|Created at'), key: 'created_at', datetime: true }, @@ -152,42 +193,67 @@ export default (props) => { // return false; // } // } - + // console.log(data, "DATA") return ( - {loading == true ?
: - error == true ? -
- - - {emptyContentAlt} -

{t("MicroservicesContainer|Can't establish websocket connection, data can't be loaded")}

-
-
-
- : - (data["parsingError"] == true) ? -
-
{t("MicroservicesContainer|Can't display data due to parsing error")}
-
+ {loading == true ? + + +
+ {t("MicroservicesContainer|Services")} +
+
+ + + +
: - - - {data && Object.keys(data).map((key, idx) => { -
- {key}: + +
+ {t("MicroservicesContainer|Services")} +
+
+ +
+ {emptyContentAlt} -
- })} -
+

{t("MicroservicesContainer|Can't establish websocket connection, data can't be loaded")}

+
+
+
+ : + + +
+ {t("MicroservicesContainer|Services")} +
+
+ + {(data["parsingError"] == true) ? +
+
{t("MicroservicesContainer|Can't display data due to parsing error")}
+
+ : + data && Object.keys(data).map((key, idx) => { + return(
+ {key}: +
) + }) + } +
}
From 01f2af8693c8a693207acd739556dc7111c3321c Mon Sep 17 00:00:00 2001 From: fpesek Date: Tue, 27 Sep 2022 09:59:40 +0200 Subject: [PATCH 07/33] Add visual indication for statuses of microservices --- .../MicroservicesContainer.js | 51 +++++++++++++++++-- .../microservices.scss | 35 +++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js index 321aebf3f..0e6670476 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import ReactJson from 'react-json-view'; import { useSelector } from 'react-redux'; -import { Container, Card, CardBody, CardHeader } from 'reactstrap'; +import { Container, Card, CardBody, CardHeader, Row } from 'reactstrap'; import { DataTable, Spinner } from 'asab-webui'; @@ -139,6 +139,50 @@ export default (props) => { }; } + + // Generate status + /* + Cannot use generateStatus func export from utils, cause it causes + "Rendered more hooks than during the previous render." error + */ + const generateStatus = (status) => { + + if (status === undefined) { + return (
); + } + if (typeof status === "string") { + return statusTranslations(status); + } + if (typeof status === "object") { + return statusTranslations(status.name); + } + return status; + } + + // Translate well known statuses + const statusTranslations = (status) => { + + if (status.toLowerCase() === "running") { + return (
); + }; + if (status.toLowerCase() === "paused") { + return (
); + }; + if (status.toLowerCase() === "restarting") { + return (
); + }; + if (status.toLowerCase() === "oomkilled") { + return (
); + }; + if (status.toLowerCase() === "dead") { + return (
); + }; + if (status.toLowerCase() === "modelled") { + return (
); + }; + return (
); + } + // const headers = [ // // { // // name: " ", @@ -204,7 +248,7 @@ export default (props) => {
- +
: @@ -241,7 +285,8 @@ export default (props) => { : data && Object.keys(data).map((key, idx) => { return(
- {key}: {generateStatus(data[key]?.state)}
{key}
+ Date: Tue, 27 Sep 2022 13:03:44 +0200 Subject: [PATCH 08/33] Rename microservices module to instances, remove obsolete containers --- doc/asab-microservices.md | 20 +-- .../InstancesContainer.js} | 38 ++--- .../InstancesContainers/instances.scss} | 18 +-- .../index.js | 30 ++-- .../AttentionCard/JSONObject.js | 35 ----- .../AttentionCard/JSONTable.js | 55 ------- .../AttentionCard/index.js | 38 ----- .../MicroserviceDetailContainer.js | 139 ------------------ 8 files changed, 38 insertions(+), 335 deletions(-) rename src/modules/maintenance/{MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js => InstancesModule/InstancesContainers/InstancesContainer.js} (85%) rename src/modules/maintenance/{MicroservicesModule/MicroservicesContainers/microservices.scss => InstancesModule/InstancesContainers/instances.scss} (63%) rename src/modules/maintenance/{MicroservicesModule => InstancesModule}/index.js (53%) delete mode 100644 src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONObject.js delete mode 100644 src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONTable.js delete mode 100644 src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/index.js delete mode 100644 src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroserviceDetailContainer.js diff --git a/doc/asab-microservices.md b/doc/asab-microservices.md index 486671cb7..8f0ae131e 100644 --- a/doc/asab-microservices.md +++ b/doc/asab-microservices.md @@ -1,14 +1,10 @@ -# ASAB Microservices +# ASAB Instances -ASAB WebUI Microservices is a page with a list of available microservices. It contains information about their hosts and launch time. By clicking on the particular microservice you will be able to watch the content of that microservice. - -## Attention required — yellow flag - -In case some microservice contains mistakes, a yellow flag will appear near the title. +ASAB WebUI Instances is a page with a list of available instances. It use a websocket connection, so the data are propagated realtime. ## Setup -In `config` file, define ASAB Microservices as a service: +In `config` file, define ASAB Instances as a service: ``` module.exports = { @@ -20,10 +16,10 @@ module.exports = { webpackDevServer: { port: 3000, proxy: { - '/api/lmio_correlator_builder': { + '/api/lmio_remote_control': { target: 'http://localhost:8086', - pathRewrite: {'^/api/lmio_correlator_builder' : ''}, - ws: true + ws: true, + pathRewrite: {'^/api/lmio_remote_control' : ''} }, } } @@ -37,8 +33,8 @@ const modules = []; ... -import ASABMicroservicesModule from 'asab-webui/modules/maintenance/MicroservicesModule'; -modules.push(ASABMicroservicesModule); +import ASABInstancesModule from 'asab-webui/modules/maintenance/InstancesModule'; +modules.push(ASABInstancesModule); ... diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js similarity index 85% rename from src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js rename to src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 0e6670476..c02333785 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -7,7 +7,7 @@ import { Container, Card, CardBody, CardHeader, Row } from 'reactstrap'; import { DataTable, Spinner } from 'asab-webui'; -export default (props) => { +export default function InstancesContainer(props) { // const [list, setList] = useState([]); // const [page, setPage] = useState(1); // const [count, setCount] = useState(0); @@ -148,7 +148,7 @@ export default (props) => { const generateStatus = (status) => { if (status === undefined) { - return (
); + return (
); } if (typeof status === "string") { return statusTranslations(status); @@ -163,24 +163,18 @@ export default (props) => { const statusTranslations = (status) => { if (status.toLowerCase() === "running") { - return (
); + return (
); }; - if (status.toLowerCase() === "paused") { - return (
); + if (status.toLowerCase() === "starting") { + return (
); }; - if (status.toLowerCase() === "restarting") { - return (
); + if (status.toLowerCase() === "stopped") { + return (
); }; - if (status.toLowerCase() === "oomkilled") { - return (
); + if (status.toLowerCase() === "unknown") { + return (
); }; - if (status.toLowerCase() === "dead") { - return (
); - }; - if (status.toLowerCase() === "modelled") { - return (
); - }; - return (
); + return (
); } // const headers = [ @@ -244,7 +238,7 @@ export default (props) => {
- {t("MicroservicesContainer|Services")} + {t("InstancesContainer|Instances")}
@@ -256,7 +250,7 @@ export default (props) => {
- {t("MicroservicesContainer|Services")} + {t("InstancesContainer|Instances")}
@@ -266,7 +260,7 @@ export default (props) => { alt={emptyContentAlt} style={{maxWidth: "38%"}} /> -

{t("MicroservicesContainer|Can't establish websocket connection, data can't be loaded")}

+

{t("InstancesContainer|Can't establish websocket connection, data can't be loaded")}

@@ -274,13 +268,13 @@ export default (props) => {
- {t("MicroservicesContainer|Services")} + {t("InstancesContainer|Services")}
- + {(data["parsingError"] == true) ?
-
{t("MicroservicesContainer|Can't display data due to parsing error")}
+
{t("InstancesContainer|Can't display data due to parsing error")}
: data && Object.keys(data).map((key, idx) => { diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss similarity index 63% rename from src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss rename to src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index 9ddd7b344..bb4966cd8 100644 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -5,7 +5,7 @@ } -.microservices-body { +.instances-body { overflow: auto; } @@ -20,26 +20,14 @@ border-radius: 50% } -.status-modelled { - background: $info; -} - .status-running { background: $light-green; } -.status-paused { +.status-starting { background: $warning; } -.status-oomkilled { +.status-stopped { background: $danger; } - -.status-undefined { - background: $white-night; -} - -.status-restarting { - background: $primary; -} \ No newline at end of file diff --git a/src/modules/maintenance/MicroservicesModule/index.js b/src/modules/maintenance/InstancesModule/index.js similarity index 53% rename from src/modules/maintenance/MicroservicesModule/index.js rename to src/modules/maintenance/InstancesModule/index.js index dcdfb28e9..298fe0a50 100644 --- a/src/modules/maintenance/MicroservicesModule/index.js +++ b/src/modules/maintenance/InstancesModule/index.js @@ -1,28 +1,20 @@ import React, { Component } from 'react'; import Module from 'asab-webui/abc/Module'; -import MicroservicesContainer from "./MicroservicesContainers/MicroservicesContainer"; -import MicroserviceDetailContainer from "./MicroservicesContainers/MicroserviceDetailContainer"; +import InstancesContainer from "./InstancesContainers/InstancesContainer"; -import "./MicroservicesContainers/microservices.scss"; +import "./InstancesContainers/instances.scss"; -export default class MicroservicesModule extends Module { +export default class InstancesModule extends Module { constructor(app, name) { - super(app, "ASABMicroservicesModule"); + super(app, "ASABInstancesModule"); app.Router.addRoute({ - path: "/microservices/svcs", + path: "/instances", exact: true, - name: "Microservices", - component: MicroservicesContainer, + name: "Instances", + component: InstancesContainer, }); - app.Router.addRoute({ - path: "/microservices/svcs/:svc_name", - exact: true, - name: "Microservice", - component: MicroserviceDetailContainer, - }) - // Check presence of Maintenance item in sidebar let items = app.Navigation.getItems()?.items; let isMaintenancePresent = false; @@ -30,8 +22,8 @@ export default class MicroservicesModule extends Module { // If Maintenance present, then append Microservices as a Maintenance subitem if (itm?.name == "Maintenance") { itm.children.push({ - name: "Microservices", - url: "/microservices/svcs", + name: "Instances", + url: "/instances", icon: "cil-list" }); isMaintenancePresent = true; @@ -45,8 +37,8 @@ export default class MicroservicesModule extends Module { icon: "cil-apps-settings", children: [ { - name: "Microservices", - url: "/microservices/svcs", + name: "Instances", + url: "/instances", icon: "cil-list" } ] diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONObject.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONObject.js deleted file mode 100644 index fccad6303..000000000 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONObject.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -import { Table } from 'reactstrap'; -import { DateTime } from 'asab-webui'; - -import { isObject, isDate } from './JSONTable'; - -const JSONObject = ({ data }) => { - const headers = Object.keys(data); - - return ( - - - {headers.map((header, idx) => { - let value = data[header]; - - if (isDate(value)) { - value = - } - else if (isObject(value) || Array.isArray(value)) { - value = - } - - return ( - - - - - )})} - -
{header}{value}
- ) -} - -export default JSONObject; \ No newline at end of file diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONTable.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONTable.js deleted file mode 100644 index 72beb800a..000000000 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/JSONTable.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { Table } from 'reactstrap'; -import { DateTime } from 'asab-webui'; - -import JSONObject from './JSONObject'; - -export const isObject = value => Object.prototype.toString.call(value) === "[object Object]"; -export const isDate = value => typeof value === 'string' && value[value.length-1] === "Z" && !isNaN(new Date(value)) - -const JSONTable = ({ data }) => { - const headers = []; - - data.forEach(obj => { - Object.keys(obj).forEach(key => { - if (headers.indexOf(key) === -1) - headers.push(key); - }) - }) - - - return ( - - - - {headers.map((header, idx) => - - )} - - - - {data.map((obj, dataIdx) => ( - - {headers.map((header, headerIdx) => { - let value=obj[header]; - - if (isDate(value)) { - value = - } - else if (isObject(value) || Array.isArray(value)) { - value = - } - - return headerIdx === 0 ? ( - - ) : - })} - - ))} - -
{header}
{value}{value}
- ); -} - -export default JSONTable; diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/index.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/index.js deleted file mode 100644 index 08e8e522b..000000000 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/AttentionCard/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - Card, CardBody, - CardHeader, Col, Row - } from 'reactstrap'; - -import JSONTable from './JSONTable'; - -const AttentionCard = ({ attention }) => { - const { t } = useTranslation(); - - const formattedAttention = []; - - Object.keys(attention).forEach(key => { - formattedAttention.push({ "id": key, ...attention[key] }) - }) - - return ( - - - - {t("MicroserviceDetailContainer|Attention")} - - - - - - - - - - - ) -} - -export default AttentionCard; \ No newline at end of file diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroserviceDetailContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroserviceDetailContainer.js deleted file mode 100644 index f2c5d30d3..000000000 --- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroserviceDetailContainer.js +++ /dev/null @@ -1,139 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -import { - Container, Card, CardBody, - CardHeader, Col, Row - } from 'reactstrap'; -import { DateTime } from 'asab-webui'; -import ReactJSON from 'react-json-view'; - -import AttentionCard from './AttentionCard'; - -const MicroserviceDetailContainer = (props) => { - const theme = useSelector(state => state.theme); - const [svc, setSvc] = useState(null); - const [error, setError] = useState(null); - const { t } = useTranslation(); - const { svc_name } = useParams(); - - const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); - - useEffect(() => { - getMicroservice(); - }, []); - - const getMicroservice = async () => { - try { - const response = await LMIORemoteControlAPI.get(`microservice/${svc_name}`); - - if (response.data.result !== "OK") throw new Error(response); - - setSvc(response.data.data); - } catch (e) { - if (e.response.status == 400) { - console.error(e.response.message); - props.app.addAlert("warning", t("MicroserviceDetailContainer|This microservice doesn't exist")); - setError("This microservice doesn't exist"); - } - else { - console.error("Failed to get service\n", e); - props.app.addAlert("warning", t("MicroserviceDetailContainer|Failed to get the microservice")); - setError("Something went wrong and this microservice wasn't found") - } - } - } - - return ( - - - - - -
- {svc?.appclass || t("MicroserviceDetailContainer|Service")} -
-
- - {error ? ( -
-
{t(`MicroserviceDetailContainer|${error}`)}
-
- ) : ( - <> - - {t("MicroserviceDetailContainer|ID")} - - { svc_name?.toString() ?? 'N/A'} - - - - {t("MicroserviceDetailContainer|Host")} - - {svc?.hostname?.toString() ?? 'N/A'} - - - - {t("MicroserviceDetailContainer|Server")} - - {svc?.servername?.toString() ?? 'N/A'} - - - - {t("MicroserviceDetailContainer|Launch time")} - - {svc?.launchtime ? : 'N/A'} - - - - {t("MicroserviceDetailContainer|Created at")} - - {svc?.created_at ? : 'N/A'} - - - - {t("MicroserviceDetailContainer|Version")} - {svc?.version ?? 'N/A'} - - - )} -
-
- -
- - {svc && ( - - - - -
- {t("MicroserviceDetailContainer|Detail")} -
-
- - - - - - - -
- -
- )} - {svc && svc["attention_required"] && Object.keys(svc["attention_required"]).length > 0 && ( - - )} -
- ) -} - -export default MicroserviceDetailContainer; \ No newline at end of file From ab286fcd0ac523821d8fec4d3ff10058daa5350e Mon Sep 17 00:00:00 2001 From: fpesek Date: Fri, 30 Sep 2022 16:45:30 +0200 Subject: [PATCH 09/33] Implement better data handling for render --- .../InstancesContainers/InstancesContainer.js | 111 +++++------------- 1 file changed, 30 insertions(+), 81 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index c02333785..5c44b4762 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -8,17 +8,13 @@ import { Container, Card, CardBody, CardHeader, Row } from 'reactstrap'; import { DataTable, Spinner } from 'asab-webui'; export default function InstancesContainer(props) { - // const [list, setList] = useState([]); - // const [page, setPage] = useState(1); - // const [count, setCount] = useState(0); - // const [filter, setFilter] = useState(""); - // const [limit, setLimit] = useState(20); const [fullFrameData, setFullFrameData] = useState({}); const [fFData, setFFData] = useState(false); const [deltaFrameData, setDeltaFrameData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const [renderData, setRenderData] = useState(false); const emptyContentImg = props.app.Config.get('brand_image').full; const emptyContentAlt = props.app.Config.get('title'); @@ -59,6 +55,8 @@ export default function InstancesContainer(props) { // Use memo for data rendering (due to expensive caluclations) const data = useMemo(() => { + // Set render data back to false + setRenderData(false); if (fFData == true) { // Render full frame data setFFData(false); @@ -67,28 +65,32 @@ export default function InstancesContainer(props) { // Render delta frame data if(deltaFrameData && Object.keys(deltaFrameData)) { // If key in full frame, update data, otherwise append deltaframe to fullframe object without mutating the original object - if (fullFrameData[Object.keys(deltaFrameData)[0]]) { - // New additions / updates to values of object - const additions = Object.values(deltaFrameData) ? Object.values(deltaFrameData)[0] : {}; - // Append new values / updates to values of object - let updateValues = {...fullFrameData[Object.keys(deltaFrameData)[0]], ...additions} - // Create a new key-value pair from deltaFrame key and new/updated values - let newObj = {}; - newObj[Object.keys(deltaFrameData)[0]] = updateValues; - // Append new object to fullFrame data object without mutation of original one - let renderAll = {...fullFrameData, ...newObj}; - // Return object - return renderAll; - } else { - // Append deltaFrame object to fullFrame object and return it - let renderAll = {...fullFrameData, ...deltaFrameData}; - return renderAll; - } + let renderAll = {...fullFrameData, ...{}}; + Object.keys(deltaFrameData).map((dfd, idx) => { + if (fullFrameData[dfd]) { + // New additions / updates to values of object + const additions = deltaFrameData[dfd] ? deltaFrameData[dfd] : {}; + // Append new values / updates to values of object + let updateValues = {...renderAll[dfd], ...additions} + // Create a new key-value pair from deltaFrame key and new/updated values + let newObj = {}; + newObj[dfd] = updateValues; + // Append new object to fullFrame data object without mutation of original one + renderAll = {...renderAll, ...newObj}; + } else { + let newObj = {}; + newObj[dfd] = deltaFrameData[dfd]; + // Append deltaFrame object to fullFrame object and return it + renderAll = {...renderAll, ...newObj}; + } + }) + setFullFrameData(renderAll); + return renderAll; } // Fallback if deltaFrameData will not meet the condition requirements return fullFrameData; } - }, [fullFrameData, deltaFrameData]) + }, [renderData == true]) // If renderData == true, then trigger computation of data rendering const reconnect = () => { if (WSClient != null) { @@ -121,6 +123,8 @@ export default function InstancesContainer(props) { // Set delta frame data setDeltaFrameData(retrievedData); } + // Set render data to trigger memo computation + setRenderData(true); } } else { const err = {}; @@ -142,12 +146,11 @@ export default function InstancesContainer(props) { // Generate status /* - Cannot use generateStatus func export from utils, cause it causes + Cannot use generateStatus func from separate container, cause it causes "Rendered more hooks than during the previous render." error */ const generateStatus = (status) => { - - if (status === undefined) { + if (status == undefined) { return (
); } if (typeof status === "string") { @@ -177,61 +180,7 @@ export default function InstancesContainer(props) { return (
); } - // const headers = [ - // // { - // // name: " ", - // // customHeaderStyle: { width: '2.5rem' }, - // // customComponent: { - // // generate: (obj) => { - // // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) { - // // return ( - // // - // // ) - // // } - // // } - // // } - // // }, - // { name: t('MicroservicesContainer|ID'), key: 'id' }, - // { name: t('MicroservicesContainer|Host'), key: 'hostname' }, - // { name: t('MicroservicesContainer|Launch time'), key: 'launchtime', datetime: true }, - // { name: t('MicroservicesContainer|Created at'), key: 'created_at', datetime: true }, - // { name: t('MicroservicesContainer|Version'), key: 'version'} - // ]; - - // // Filter the value - // const onSearch = (value) => { - // setFilter(value); - // }; - - // useEffect(() => { - // getMicroservicesList(); - // }, [page, filter ,limit]); - - // const getMicroservicesList = async () => { - // try { - // const response = await LMIORemoteControlAPI.get('/microservices', { params: { p: page, i: limit, f: filter }}); - - // if (response.data.result !== "OK") throw new Error(response); - - // setList(response.data.data); - // setCount(response.data.count); - // } catch (e) { - // console.error(e); - // props.app.addAlert("warning", t('MicroservicesContainer|Failed to fetch data')); - // } - // } - // const customRowStyle = { - // style: { - // backgroundColor: "#fff3cd", - // color: "#856404" - // }, - // condition: (obj) => { - // if (obj["attention_required"] && Object.keys(obj["attention_required"]).length > 0) return true; - // return false; - // } - // } - // console.log(data, "DATA") return ( {loading == true ? @@ -279,7 +228,7 @@ export default function InstancesContainer(props) { : data && Object.keys(data).map((key, idx) => { return(
- {generateStatus(data[key]?.state)}
{key}
+ {generateStatus(data[key]?.state ? data[key].state : undefined)}
{key}
Date: Mon, 3 Oct 2022 10:13:19 +0200 Subject: [PATCH 10/33] Unify data storing and rendering for full and delta frame data --- .../InstancesContainers/InstancesContainer.js | 79 ++++++++----------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 5c44b4762..23e3baa3f 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -10,11 +10,9 @@ import { DataTable, Spinner } from 'asab-webui'; export default function InstancesContainer(props) { const [fullFrameData, setFullFrameData] = useState({}); - const [fFData, setFFData] = useState(false); - const [deltaFrameData, setDeltaFrameData] = useState({}); + const [wsData, setWSData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const [renderData, setRenderData] = useState(false); const emptyContentImg = props.app.Config.get('brand_image').full; const emptyContentAlt = props.app.Config.get('title'); @@ -55,42 +53,35 @@ export default function InstancesContainer(props) { // Use memo for data rendering (due to expensive caluclations) const data = useMemo(() => { - // Set render data back to false - setRenderData(false); - if (fFData == true) { - // Render full frame data - setFFData(false); - return fullFrameData; - } else { - // Render delta frame data - if(deltaFrameData && Object.keys(deltaFrameData)) { - // If key in full frame, update data, otherwise append deltaframe to fullframe object without mutating the original object - let renderAll = {...fullFrameData, ...{}}; - Object.keys(deltaFrameData).map((dfd, idx) => { - if (fullFrameData[dfd]) { - // New additions / updates to values of object - const additions = deltaFrameData[dfd] ? deltaFrameData[dfd] : {}; - // Append new values / updates to values of object - let updateValues = {...renderAll[dfd], ...additions} - // Create a new key-value pair from deltaFrame key and new/updated values - let newObj = {}; - newObj[dfd] = updateValues; - // Append new object to fullFrame data object without mutation of original one - renderAll = {...renderAll, ...newObj}; - } else { - let newObj = {}; - newObj[dfd] = deltaFrameData[dfd]; - // Append deltaFrame object to fullFrame object and return it - renderAll = {...renderAll, ...newObj}; - } - }) - setFullFrameData(renderAll); - return renderAll; - } - // Fallback if deltaFrameData will not meet the condition requirements - return fullFrameData; + // Render ws data + if(wsData && Object.keys(wsData)) { + // If key in full frame, update data, otherwise append wsData to fullframe object without mutating the original object + let renderAll = {...fullFrameData, ...{}}; + Object.keys(wsData).map((dfd, idx) => { + if (fullFrameData[dfd]) { + // New additions / updates to values of object + const additions = wsData[dfd] ? wsData[dfd] : {}; + // Append new values / updates to values of object + let updateValues = {...renderAll[dfd], ...additions} + // Create a new key-value pair from deltaFrame key and new/updated values + let newObj = {}; + newObj[dfd] = updateValues; + // Append new object to fullFrame data object without mutation of original one + renderAll = {...renderAll, ...newObj}; + } else { + let newObj = {}; + newObj[dfd] = wsData[dfd]; + // Append wsData object to fullFrame object and return it + renderAll = {...renderAll, ...newObj}; + } + }) + // Set fullFrame + setFullFrameData(renderAll); + return renderAll; } - }, [renderData == true]) // If renderData == true, then trigger computation of data rendering + // Fallback if wsData will not meet the condition requirements + return fullFrameData; + }, [wsData]) // If websocket data change, then trigger computation of data rendering const reconnect = () => { if (WSClient != null) { @@ -115,16 +106,8 @@ export default function InstancesContainer(props) { if (IsJsonString(message.data) == true) { let retrievedData = JSON.parse(message.data); if (retrievedData && Object.keys(retrievedData)) { - if (Object.keys(retrievedData).length > 1) { - // Set full frame data - setFullFrameData(retrievedData); - setFFData(true); - } else { - // Set delta frame data - setDeltaFrameData(retrievedData); - } - // Set render data to trigger memo computation - setRenderData(true); + // Set websocket data + setWSData(retrievedData); } } else { const err = {}; From 120d8e62fbd7e0eb33d9e9c4964cddd48d07eb68 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 3 Oct 2022 13:25:53 +0200 Subject: [PATCH 11/33] Add ws data to custom table --- .../InstancesContainers/InstancesContainer.js | 192 +++++++++++------- .../InstancesContainers/instances.scss | 18 +- 2 files changed, 135 insertions(+), 75 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 23e3baa3f..9f0f2b3e4 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'; import ReactJson from 'react-json-view'; import { useSelector } from 'react-redux'; -import { Container, Card, CardBody, CardHeader, Row } from 'reactstrap'; +import { Container, Card, CardBody, CardHeader, Table } from 'reactstrap'; -import { DataTable, Spinner } from 'asab-webui'; +import { CellContentLoader } from 'asab-webui'; export default function InstancesContainer(props) { @@ -13,24 +13,24 @@ export default function InstancesContainer(props) { const [wsData, setWSData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - - const emptyContentImg = props.app.Config.get('brand_image').full; - const emptyContentAlt = props.app.Config.get('title'); + const [errorMsg, setErrorMsg] = useState(""); const theme = useSelector(state => state.theme); const { t } = useTranslation(); + // TODO: implement axios calls for post and put requests when ready on BE + // const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); + // Set up websocket connection let wsSubPath = '/ws'; - // const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); const serviceName = 'lmio_remote_control'; let WSUrl = props.app.getWebSocketURL(serviceName, wsSubPath); let WSClient = null; const isMounted = useRef(null); - + // Connect to ws on page initialization, close ws connection on page leave useEffect(() => { isMounted.current = true; @@ -83,6 +83,7 @@ export default function InstancesContainer(props) { return fullFrameData; }, [wsData]) // If websocket data change, then trigger computation of data rendering + // Reconnect ws method const reconnect = () => { if (WSClient != null) { try { @@ -109,16 +110,16 @@ export default function InstancesContainer(props) { // Set websocket data setWSData(retrievedData); } + setError(false); } else { - const err = {}; - err["parsingError"] = true; - setData(err); + setErrorMsg(t("InstancesContainer|Can't display data due to parsing error")); + setError(true); } - setError(false); }; WSClient.onerror = (error) => { setLoading(false); + setErrorMsg(t("InstancesContainer|Can't establish websocket connection, data can't be loaded")); setError(true); setTimeout(() => { reconnect(); @@ -166,67 +167,114 @@ export default function InstancesContainer(props) { return ( - {loading == true ? - - -
- {t("InstancesContainer|Instances")} -
-
- -
-
-
- : - error == true ? - - -
- {t("InstancesContainer|Instances")} -
-
- -
- {emptyContentAlt} -

{t("InstancesContainer|Can't establish websocket connection, data can't be loaded")}

-
-
-
- : - - -
- {t("InstancesContainer|Services")} -
-
- - {(data["parsingError"] == true) ? -
-
{t("InstancesContainer|Can't display data due to parsing error")}
-
- : - data && Object.keys(data).map((key, idx) => { - return(
- {generateStatus(data[key]?.state ? data[key].state : undefined)}
{key}
- -
) - }) - } -
-
- } + + +
+ {t("InstancesContainer|Instances")} +
+
+ + {(loading == true) ? + + : + + + + + + + + + + + + + + + + + + + + + + + {(error == true) ? + + + + : + data && Object.keys(data).map((key, idx) => { + return( + + + + + + + + + + + ) + } + )} + +
+ + {t("InstancesContainer|Name")} + + {t("InstancesContainer|Service")} + + {t("InstancesContainer|Node")} + + {t("InstancesContainer|Instance")} + + {t("InstancesContainer|Type")} +
{errorMsg}
+ {generateStatus(data[key]?.state ? data[key].state : undefined)} + + {data[key]?.name} + + {data[key]?.service} + + {data[key]?.node} + + {data[key]?.instance_id ? data[key].instance_id : key} + + {data[key]?.type} + + {data[key]?.docker_data ? + + : + null + } + + {data[key]?.advertised_data ? + + : + null + } +
+ } +
+
) } diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index bb4966cd8..345220606 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -1,19 +1,24 @@ @import "~asab-webui/styles/constants/index.scss"; +/* container styles */ + .svcs-container { height: 100%; } +/* card body styles */ + .instances-body { overflow: auto; + padding-top: 0 !important; + padding-bottom: 0 !important; } -.spinner { - padding-top: 5rem; -} +/* status indicator styles */ .status-circle { + margin-top: 0.25rem; width: 0.8rem; height: 0.8rem; background: $secondary; @@ -31,3 +36,10 @@ .status-stopped { background: $danger; } + +/* table styles */ + +.td-style { + text-align: center; + background: $bg-color; +} From 2394412d505b44aebac5a5c41d2a420ae92343d0 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 3 Oct 2022 16:20:03 +0200 Subject: [PATCH 12/33] Implement filtering --- .../InstancesContainers/InstancesContainer.js | 261 +++++++++++------- 1 file changed, 163 insertions(+), 98 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 9f0f2b3e4..b47c0dd52 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -3,7 +3,9 @@ import { useTranslation } from 'react-i18next'; import ReactJson from 'react-json-view'; import { useSelector } from 'react-redux'; -import { Container, Card, CardBody, CardHeader, Table } from 'reactstrap'; +import { Container, Card, CardBody, CardHeader, Table, + InputGroup, InputGroupText, Input, InputGroupAddon +} from 'reactstrap'; import { CellContentLoader } from 'asab-webui'; @@ -15,6 +17,8 @@ export default function InstancesContainer(props) { const [error, setError] = useState(false); const [errorMsg, setErrorMsg] = useState(""); + const [filter, setFilter] = useState(""); + const theme = useSelector(state => state.theme); const { t } = useTranslation(); @@ -51,6 +55,7 @@ export default function InstancesContainer(props) { } }, []); + // Use memo for data rendering (due to expensive caluclations) const data = useMemo(() => { // Render ws data @@ -83,6 +88,24 @@ export default function InstancesContainer(props) { return fullFrameData; }, [wsData]) // If websocket data change, then trigger computation of data rendering + + // Filter state among data + const filteredData = useMemo (() => { + if ((filter != undefined) && (filter.length > 0)) { + const fltr = filter.toLowerCase(); + let filteredObj = {}; + Object.keys(fullFrameData).map((key, idx) => { + if (fullFrameData[key].state) { + if (fullFrameData[key].state.indexOf(fltr) != -1) { + filteredObj[key] = fullFrameData[key]; + } + } + }) + return filteredObj; + } + return undefined; + }, [filter, fullFrameData]) + // Reconnect ws method const reconnect = () => { if (WSClient != null) { @@ -128,43 +151,6 @@ export default function InstancesContainer(props) { } - // Generate status - /* - Cannot use generateStatus func from separate container, cause it causes - "Rendered more hooks than during the previous render." error - */ - const generateStatus = (status) => { - if (status == undefined) { - return (
); - } - if (typeof status === "string") { - return statusTranslations(status); - } - if (typeof status === "object") { - return statusTranslations(status.name); - } - return status; - } - - // Translate well known statuses - const statusTranslations = (status) => { - - if (status.toLowerCase() === "running") { - return (
); - }; - if (status.toLowerCase() === "starting") { - return (
); - }; - if (status.toLowerCase() === "stopped") { - return (
); - }; - if (status.toLowerCase() === "unknown") { - return (
); - }; - return (
); - } - - return ( @@ -172,6 +158,11 @@ export default function InstancesContainer(props) {
{t("InstancesContainer|Instances")}
+ {(loading == true) ? @@ -179,7 +170,7 @@ export default function InstancesContainer(props) { : - + @@ -192,9 +183,6 @@ export default function InstancesContainer(props) { - @@ -202,11 +190,20 @@ export default function InstancesContainer(props) { {t("InstancesContainer|Node")} + + + @@ -215,61 +212,8 @@ export default function InstancesContainer(props) { : - data && Object.keys(data).map((key, idx) => { - return( - - - - - - - - - - - ) - } - )} + + }
- {t("InstancesContainer|Name")} - {t("InstancesContainer|Service")} - {t("InstancesContainer|Instance")} + {t("InstancesContainer|Name")} {t("InstancesContainer|Type")} + {t("InstancesContainer|Version")} + + {t("InstancesContainer|Docker data")} + + {t("InstancesContainer|Advertised data")} +
{errorMsg}
- {generateStatus(data[key]?.state ? data[key].state : undefined)} - - {data[key]?.name} - - {data[key]?.service} - - {data[key]?.node} - - {data[key]?.instance_id ? data[key].instance_id : key} - - {data[key]?.type} - - {data[key]?.docker_data ? - - : - null - } - - {data[key]?.advertised_data ? - - : - null - } -
} @@ -279,6 +223,127 @@ export default function InstancesContainer(props) { ) } +// Method to render table row with data +const DataRow = ({data}) => { + const { t } = useTranslation(); + const theme = useSelector(state => state.theme); + + // Generate status + /* + Cannot use generateStatus func from separate container, cause it causes + "Rendered more hooks than during the previous render." error + */ + const generateStatus = (status) => { + if (status == undefined) { + return (
); + } + if (typeof status === "string") { + return statusTranslations(status); + } + if (typeof status === "object") { + return statusTranslations(status.name); + } + return status; + } + + // Translate well known statuses + const statusTranslations = (status) => { + + if (status.toLowerCase() === "running") { + return (
); + }; + if (status.toLowerCase() === "starting") { + return (
); + }; + if (status.toLowerCase() === "stopped") { + return (
); + }; + if (status.toLowerCase() === "unknown") { + return (
); + }; + return (
); + } + + return( + data && Object.keys(data).map((key, idx) => ( + + + {generateStatus(data[key]?.state ? data[key].state : undefined)} + + + {data[key]?.service} + + + {data[key]?.node} + + + {data[key]?.name} + + + {data[key]?.type} + + + {data[key]?.version} + + + {data[key]?.docker_data ? + + : + null + } + + + {data[key]?.advertised_data ? + + : + null + } + + + )) + ) +} + + +// Search method +const Search = ({ search, filterValue, setFilterValue }) => { + + return ( +
+ + {search.icon && + + + + } + setFilterValue(e.target.value)} + placeholder={search.placeholder} + type="text" + bsSize="sm" + /> + +
+ ); +} + // Check if string is valid JSON function IsJsonString(str) { try { From c8c41fefdc5723f40e6745e69049908cea07d71a Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 3 Oct 2022 16:21:29 +0200 Subject: [PATCH 13/33] Update placeholder in search --- .../InstancesModule/InstancesContainers/InstancesContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index b47c0dd52..899cafd41 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -159,7 +159,7 @@ export default function InstancesContainer(props) { {t("InstancesContainer|Instances")}
From 1cc75cb8ec8c599d32a37a07af079b70ffeeefc9 Mon Sep 17 00:00:00 2001 From: fpesek Date: Tue, 25 Oct 2022 16:01:27 +0200 Subject: [PATCH 14/33] Option to display version from advertised data --- .../InstancesModule/InstancesContainers/InstancesContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 899cafd41..657e624d3 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -283,7 +283,7 @@ const DataRow = ({data}) => { {data[key]?.type} - {data[key]?.version} + {data[key]?.advertised_data?.version ? data[key]?.advertised_data?.version : data[key]?.version ? data[key]?.version : "N/A"} {data[key]?.docker_data ? From 8e3d55ef86c0b6e4cc89b5dbd7afc0a144674014 Mon Sep 17 00:00:00 2001 From: fpesek Date: Tue, 25 Oct 2022 17:15:05 +0200 Subject: [PATCH 15/33] Update table with collapsed info with detil and advertised data --- .../InstancesContainers/InstancesContainer.js | 127 +++++++++++------- .../InstancesContainers/instances.scss | 14 ++ 2 files changed, 91 insertions(+), 50 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 657e624d3..658f2ba5c 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -170,14 +170,12 @@ export default function InstancesContainer(props) { : - + - - @@ -198,12 +196,6 @@ export default function InstancesContainer(props) { - - @@ -226,7 +218,6 @@ export default function InstancesContainer(props) { // Method to render table row with data const DataRow = ({data}) => { const { t } = useTranslation(); - const theme = useSelector(state => state.theme); // Generate status /* @@ -265,59 +256,95 @@ const DataRow = ({data}) => { } return( - data && Object.keys(data).map((key, idx) => ( - - - + data && Object.keys(data).map((objKey) => ( + + )) + ) +} + +const RowContent = ({objKey, data, generateStatus}) => { + const { t } = useTranslation(); + const theme = useSelector(state => state.theme); + const [collapseData, setCollapseData] = useState(data[objKey]?.state && data[objKey]?.state == "stopped" ? false : true); + + return( + <> + - )) - ) + {!collapseData && + + + + } + + ) } diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index 345220606..03cfd42cc 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -43,3 +43,17 @@ text-align: center; background: $bg-color; } + +.collapsed-data { + background-color: $bg-color; +} + +.caret-status-div { + display: inline-flex !important; + .caret-icon { + padding-right: 0.75rem; + padding-top: 0.15rem; + color: $primary; + cursor: pointer; + } +} From e4f3544163c191d2c5eb0af7eaf3680b0a0c2118 Mon Sep 17 00:00:00 2001 From: fpesek Date: Wed, 26 Oct 2022 12:37:21 +0200 Subject: [PATCH 16/33] Implement nested table for collapsed data --- .../InstancesContainers/InstancesContainer.js | 116 ++++++++++++++---- .../InstancesContainers/instances.scss | 41 +++++++ 2 files changed, 134 insertions(+), 23 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 658f2ba5c..bb4d65626 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -168,7 +168,7 @@ export default function InstancesContainer(props) { {(loading == true) ? : -
{t("InstancesContainer|Version")} - {t("InstancesContainer|Docker data")} - - {t("InstancesContainer|Advertised data")} -
- {generateStatus(data[key]?.state ? data[key].state : undefined)} - - {data[key]?.service} -
- {data[key]?.node} +
+ {collapseData ? + {setCollapseData(false)}}> + : + {setCollapseData(true)}}> + } + {generateStatus(data[objKey]?.state ? data[objKey].state : undefined)} +
- {data[key]?.name} + {data[objKey]?.service} - {data[key]?.type} + {data[objKey]?.node} - {data[key]?.advertised_data?.version ? data[key]?.advertised_data?.version : data[key]?.version ? data[key]?.version : "N/A"} + {data[objKey]?.name} - {data[key]?.docker_data ? - - : - null - } + {data[objKey]?.type} - {data[key]?.advertised_data ? - - : - null - } + {data[objKey]?.advertised_data?.version ? data[objKey]?.advertised_data?.version : data[objKey]?.version ? data[objKey]?.version : "N/A"}
+ {data[objKey]?.detail ? + <> + + {t("InstancesContainer|Detail")} + + + + : + null + } + {data[objKey]?.advertised_data ? + <> + + {t("InstancesContainer|Advertised data")} + + + + : + null + } +
+
@@ -304,39 +304,58 @@ const RowContent = ({objKey, data, generateStatus}) => { {!collapseData && } - ) + ) +} + +const CollapsedTable = ({obj, title}) => { + const theme = useSelector(state => state.theme); + + return( +
- {data[objKey]?.detail ? - <> - - {t("InstancesContainer|Detail")} + {data[objKey]?.error ? +
+ {t("InstancesContainer|Error")}: {data[objKey]?.error?.toString()} +
+ : + null + } + {data[objKey]?.exception ? +
+ {t("InstancesContainer|Exception")}: {data[objKey]?.exception?.toString()} +
+ : + null + } + {data[objKey]?.returncode?.toString() ? +
+ {t("InstancesContainer|Return code")}: {data[objKey]?.returncode?.toString()} +
+ : + null + } + {data[objKey]?.console ? +
+ + {t("InstancesContainer|Console")}: - +
+ : + null + } + {data[objKey]?.detail ? + : null } {data[objKey]?.advertised_data ? - <> - - {t("InstancesContainer|Advertised data")} - - - + : null } @@ -344,7 +363,58 @@ const RowContent = ({objKey, data, generateStatus}) => {
+ + + + + + + {Object.keys(obj).length != 0 && Object.entries(obj).map((itms, idx) => { + return( + + + + + ) + })} + +
{title}
+ + {itms[0] && itms[0].toString()} + + + {itms[1] ? + (typeof itms[1] == "object") && (Object.keys(itms[1]).length > 0) ? + + : + {itms[1] && itms[1].toString()} + : "-"} +
+ ) } diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index 03cfd42cc..5e6bf4303 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -44,10 +44,16 @@ background: $bg-color; } +/* collapsed table styles */ + .collapsed-data { background-color: $bg-color; } +.collapsed-data:hover { + background-color: $bg-color; +} + .caret-status-div { display: inline-flex !important; .caret-icon { @@ -57,3 +63,38 @@ cursor: pointer; } } + +.collapsed-table-row { + padding-top: 0px !important; + padding-bottom: 0px !important; + td { + padding-top: 0.25em !important; + padding-bottom: 0.25em !important; + height: fit-content !important; + } + th { + padding-top: 0.25em !important; + padding-bottom: 0.25em !important; + padding-left: 0em !important; + height: fit-content !important; + } +} + +.collapsed-heading { + color: var(--text-secondary-color) !important; + text-align: inherit !important; + font-weight: bold !important; +} + +.collapsed-code-value { + font-size: 100%; + color: var(--text-color); +} + +.collapsed-console { + display: inline-flex; +} + +.collapsed-span { + padding-right: 0.5em; +} From b8bcaa7786f399ce9b3553ac1956ed5445c44f3d Mon Sep 17 00:00:00 2001 From: fpesek Date: Wed, 26 Oct 2022 12:41:25 +0200 Subject: [PATCH 17/33] Refactor exception to handle objects instead of strings --- .../InstancesContainers/InstancesContainer.js | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index bb4d65626..8742f30ae 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -304,23 +304,34 @@ const RowContent = ({objKey, data, generateStatus}) => { {!collapseData && - {data[objKey]?.error ? + {data[objKey]?.returncode?.toString() ?
- {t("InstancesContainer|Error")}: {data[objKey]?.error?.toString()} + {t("InstancesContainer|Return code")}: {data[objKey]?.returncode?.toString()}
: null } - {data[objKey]?.exception ? + {data[objKey]?.error ?
- {t("InstancesContainer|Exception")}: {data[objKey]?.exception?.toString()} + {t("InstancesContainer|Error")}: {data[objKey]?.error?.toString()}
: null } - {data[objKey]?.returncode?.toString() ? -
- {t("InstancesContainer|Return code")}: {data[objKey]?.returncode?.toString()} + {data[objKey]?.exception ? +
+ + {t("InstancesContainer|Exception")}: + +
: null From 75e2cd580585720383084a200f6e0db7223b6920 Mon Sep 17 00:00:00 2001 From: fpesek Date: Wed, 26 Oct 2022 15:33:19 +0200 Subject: [PATCH 18/33] Add action buttons foritems container actions --- .../InstancesContainers/InstancesContainer.js | 92 +++++++++++++++++-- .../components/ActionButton.js | 46 ++++++++++ .../InstancesContainers/instances.scss | 8 ++ 3 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 8742f30ae..61720e6d8 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -4,11 +4,14 @@ import ReactJson from 'react-json-view'; import { useSelector } from 'react-redux'; import { Container, Card, CardBody, CardHeader, Table, - InputGroup, InputGroupText, Input, InputGroupAddon + InputGroup, InputGroupText, Input, InputGroupAddon, + ButtonGroup, Button } from 'reactstrap'; import { CellContentLoader } from 'asab-webui'; +import ActionButton from "./components/ActionButton"; + export default function InstancesContainer(props) { const [fullFrameData, setFullFrameData] = useState({}); @@ -23,9 +26,6 @@ export default function InstancesContainer(props) { const { t } = useTranslation(); - // TODO: implement axios calls for post and put requests when ready on BE - // const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); - // Set up websocket connection let wsSubPath = '/ws'; const serviceName = 'lmio_remote_control'; @@ -176,6 +176,7 @@ export default function InstancesContainer(props) { + @@ -196,15 +197,17 @@ export default function InstancesContainer(props) { {t("InstancesContainer|Version")} + + {(error == true) ? - {errorMsg} + {errorMsg} : - + } @@ -216,7 +219,7 @@ export default function InstancesContainer(props) { } // Method to render table row with data -const DataRow = ({data}) => { +const DataRow = ({data, props}) => { const { t } = useTranslation(); // Generate status @@ -262,15 +265,42 @@ const DataRow = ({data}) => { objKey={objKey} data={data} generateStatus={generateStatus} + props={props} /> )) ) } -const RowContent = ({objKey, data, generateStatus}) => { +// Content of the row in table +const RowContent = ({props, objKey, data, generateStatus}) => { const { t } = useTranslation(); const theme = useSelector(state => state.theme); - const [collapseData, setCollapseData] = useState(data[objKey]?.state && data[objKey]?.state == "stopped" ? false : true); + const [collapseData, setCollapseData] = useState(true); + + useEffect(() => { + if (data[objKey]?.state && ((data[objKey]?.state == "stopped") || (data[objKey]?.state == "starting"))) { + setCollapseData(false); + } else { + setCollapseData(true); + } + },[data[objKey]?.state]) + + // Action to start, stop, restart and up the container + const setAction = async(action, id) => { + let body = {}; + body["command"] = action; + const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); + try { + let response = await LMIORemoteControlAPI.post(`/instance/${id}`, body); + if (response.data.result != "OK") { + throw new Error(`Something went wrong, failed to ${action} container`); + } + props.app.addAlert("success", t("InstancesContainer|Instance action triggered successfully")); + } catch(e) { + console.error(e); + props.app.addAlert("warning", t("InstancesContainer|Instance action has not been triggered")); + } + } return( <> @@ -300,10 +330,51 @@ const RowContent = ({objKey, data, generateStatus}) => { {data[objKey]?.advertised_data?.version ? data[objKey]?.advertised_data?.version : data[objKey]?.version ? data[objKey]?.version : "N/A"} + +
+ + {setAction("start", data[objKey]?.instance_id)}} + outline + /> + {setAction("stop", data[objKey]?.instance_id)}} + icon="cil-ban" + /> + {setAction("restart", data[objKey]?.instance_id)}} + icon="cil-reload" + outline + /> + {setAction("up", data[objKey]?.instance_id)}} + icon="cil-arrow-thick-from-bottom" + outline + /> + +
+ {!collapseData && - + {data[objKey]?.returncode?.toString() ?
{t("InstancesContainer|Return code")}: {data[objKey]?.returncode?.toString()} @@ -377,6 +448,7 @@ const RowContent = ({objKey, data, generateStatus}) => { ) } +// Method to display collapsed table const CollapsedTable = ({obj, title}) => { const theme = useSelector(state => state.theme); diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js b/src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js new file mode 100644 index 000000000..8f7f73456 --- /dev/null +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { Button, Tooltip } from 'reactstrap'; + +const ActionButton = ({ + label, + onClick, + icon, + id, + disabled=false, + className="", + color="", + outline=false +}) => { + const [tooltipOpen, setTooltipOpen] = useState(false); + + const toggle = () => setTooltipOpen(!tooltipOpen); + + const title = () => `${label.split(' ')[0]}`; + + return ( + + + + {title()} + + + ) +} + +export default ActionButton; diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index 5e6bf4303..dc04a937d 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -98,3 +98,11 @@ .collapsed-span { padding-right: 0.5em; } + +.action-button { + border-right: 1px solid $secondary; +} + +.action-button:hover { + border-right: 1px solid $secondary; +} From 8a5422e98d809b94442d51075c3914eba3d7f316 Mon Sep 17 00:00:00 2001 From: fpesek Date: Wed, 26 Oct 2022 16:28:08 +0200 Subject: [PATCH 19/33] Add locales for ASAB Instances, rename docs --- demo/public/locales/cs/translation.json | 48 +++++++++++-------- demo/public/locales/en/translation.json | 48 +++++++++++-------- ...sab-microservices.md => asab-instances.md} | 0 .../InstancesContainers/InstancesContainer.js | 8 ++-- 4 files changed, 60 insertions(+), 44 deletions(-) rename doc/{asab-microservices.md => asab-instances.md} (100%) diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index 9c800bf26..d2f88b72b 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -108,28 +108,36 @@ "Only tar.gz files are allowed": "Povoleny jsou pouze soubory tar.gz", "Choose file": "Vyberte soubor" }, - "MicroservicesContainer": { - "ID": "ID", - "Launch time": "Čas spuštění", - "Host": "Host", - "Created at": "Vytvořeno v", + "InstancesContainer": { + "Can't display data due to parsing error": "Nelze zobrazit data kvůli chybě v parsování", + "Can't establish websocket connection, data can't be loaded": "Nepodařilo se navázat websocket spojení, data nelze načíst", + "Instances": "Instance", + "Filter state": "Filtrovat stav", + "Loading": "Načítání", + "Service": "Služba", + "Node": "Node", + "Name": "Název", + "Type": "Typ", "Version": "Verze", - "Microservices": "Mikroslužby", - "Failed to fetch data": "Nezdařilo se získat data" - }, - "MicroserviceDetailContainer" : { - "This microservice doesn't exist": "Tato mikroslužba neexistuje", - "Failed to get the microservice": "Nezdařilo se získat mikroslužbu", - "Something went wrong and this microservice wasn't found": "Něco se pokazilo a tato mikroslužba nebyla nalezena", - "Service": "Servica", + "Not defined": "Neurčeno", + "Running": "Běží", + "Starting": "Spouští se", + "Stopped": "Zastaveno", + "Unknown": "Neznámo", + "Instance action triggered successfully": "Akce instance byla úspešně spuštěna", + "Instance action has not been triggered": "Akce instance nebyla spuštěna", + "Un-collapse": "Rozbalit", + "Collapse": "Sbalit", + "Start": "Spustit", + "Stop": "Zastavit", + "Restart": "Restartovat", + "Up": "Nahodit", + "Return code": "Return code", + "Error": "Error", + "Exception": "Exception", + "Console": "Console", "Detail": "Detail", - "ID": "ID", - "Host": "Host", - "Server": "Server", - "Launch time": "Čas spuštění", - "Created at": "Vytvořeno v", - "Version": "Verze", - "Attention": "Pozor" + "Advertised data": "Advertised data" }, "UserInterfaceCard": { "User interface": "Uživatelské rozhraní", diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index 5be0b74b7..c24c83d12 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -108,28 +108,36 @@ "Only tar.gz files are allowed": "Only tar.gz files are allowed", "Choose file": "Choose file" }, - "MicroservicesContainer": { - "ID": "ID", - "Launch time": "Launch time", - "Host": "Host", - "Created at": "Created at", - "Version": "Version", - "Microservices": "Microservices", - "Failed to fetch data": "Failed to fetch data" - }, - "MicroserviceDetailContainer" : { - "This microservice doesn't exist": "This microservice doesn't exist", - "Failed to get the microservice": "Failed to get the microservice", - "Something went wrong and this microservice wasn't found": "Something went wrong and this microservice wasn't found", + "InstancesContainer": { + "Can't display data due to parsing error": "Can't display data due to parsing error", + "Can't establish websocket connection, data can't be loaded": "Can't establish websocket connection, data can't be loaded", + "Instances": "Instances", + "Filter state": "Filter state", + "Loading": "Loading", "Service": "Service", - "Detail": "Detail", - "ID": "ID", - "Host": "Host", - "Server": "Server", - "Launch time": "Launch time", - "Created at": "Created at", + "Node": "Node", + "Name": "Name", + "Type": "Type", "Version": "Version", - "Attention": "Attention" + "Not defined": "Not defined", + "Running": "Running", + "Starting": "Starting", + "Stopped": "Stopped", + "Unknown": "Unknown", + "Instance action triggered successfully": "Instance action triggered successfully", + "Instance action has not been triggered": "Instance action has not been triggered", + "Un-collapse": "Un-collapse", + "Collapse": "Collapse", + "Start": "Start", + "Stop": "Stop", + "Restart": "Restart", + "Up": "Up", + "Return code": "Return code", + "Error": "Error", + "Exception": "Exception", + "Console": "Console", + "Detail": "Detail", + "Advertised data": "Advertised data" }, "TenantSelectionCard": { "Select valid tenant to enter the application": "Select valid tenant to enter the application", diff --git a/doc/asab-microservices.md b/doc/asab-instances.md similarity index 100% rename from doc/asab-microservices.md rename to doc/asab-instances.md diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 61720e6d8..2314ecba9 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -334,7 +334,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => {
{ outline /> { icon="cil-ban" /> { outline /> {setAction("up", data[objKey]?.instance_id)}} From 684014b5b63c7ab55c7532a64f3fca464a239706 Mon Sep 17 00:00:00 2001 From: fpesek Date: Wed, 26 Oct 2022 16:57:33 +0200 Subject: [PATCH 20/33] Add simple disabling for action buttons --- .../InstancesContainers/InstancesContainer.js | 14 ++++++++++---- .../InstancesContainers/instances.scss | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 2314ecba9..82525f7ae 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -276,6 +276,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { const { t } = useTranslation(); const theme = useSelector(state => state.theme); const [collapseData, setCollapseData] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { if (data[objKey]?.state && ((data[objKey]?.state == "stopped") || (data[objKey]?.state == "starting"))) { @@ -300,6 +301,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { console.error(e); props.app.addAlert("warning", t("InstancesContainer|Instance action has not been triggered")); } + setIsSubmitting(false); } return( @@ -339,8 +341,9 @@ const RowContent = ({props, objKey, data, generateStatus}) => { className="action-button" color="primary" icon="cil-media-play" - onClick={() => {setAction("start", data[objKey]?.instance_id)}} + onClick={() => {setAction("start", data[objKey]?.instance_id), setIsSubmitting(true)}} outline + disabled={isSubmitting == true} /> { className="action-button" color="danger" outline - onClick={() => {setAction("stop", data[objKey]?.instance_id)}} + onClick={() => {setAction("stop", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-ban" + disabled={isSubmitting == true} /> {setAction("restart", data[objKey]?.instance_id)}} + onClick={() => {setAction("restart", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-reload" outline + disabled={isSubmitting == true} /> {setAction("up", data[objKey]?.instance_id)}} + onClick={() => {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-arrow-thick-from-bottom" outline + disabled={isSubmitting == true} />
diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss index dc04a937d..8f0a493c2 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss @@ -106,3 +106,7 @@ .action-button:hover { border-right: 1px solid $secondary; } + +.action-button:disabled { + border-right: 1px solid $secondary; +} From e64b2711ef5cb129c821abe0d39cc88021c22649 Mon Sep 17 00:00:00 2001 From: fpesek Date: Thu, 27 Oct 2022 17:16:38 +0200 Subject: [PATCH 21/33] Refactor delta and fullframe handling according to changes in remote control service --- .../InstancesContainers/InstancesContainer.js | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js index 82525f7ae..c2cb97fde 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js @@ -58,31 +58,39 @@ export default function InstancesContainer(props) { // Use memo for data rendering (due to expensive caluclations) const data = useMemo(() => { + let webSocketData = wsData.data; // Render ws data - if(wsData && Object.keys(wsData)) { - // If key in full frame, update data, otherwise append wsData to fullframe object without mutating the original object - let renderAll = {...fullFrameData, ...{}}; - Object.keys(wsData).map((dfd, idx) => { - if (fullFrameData[dfd]) { - // New additions / updates to values of object - const additions = wsData[dfd] ? wsData[dfd] : {}; - // Append new values / updates to values of object - let updateValues = {...renderAll[dfd], ...additions} - // Create a new key-value pair from deltaFrame key and new/updated values - let newObj = {}; - newObj[dfd] = updateValues; - // Append new object to fullFrame data object without mutation of original one - renderAll = {...renderAll, ...newObj}; - } else { - let newObj = {}; - newObj[dfd] = wsData[dfd]; - // Append wsData object to fullFrame object and return it - renderAll = {...renderAll, ...newObj}; - } - }) - // Set fullFrame - setFullFrameData(renderAll); - return renderAll; + if(webSocketData && Object.keys(webSocketData)) { + // Check for delta frame + if (wsData?.frame_type === "df") { + // If key in full frame, update data, otherwise append wsData to fullframe object without mutating the original object + let renderAll = {...fullFrameData, ...{}}; + Object.keys(webSocketData).map((dfd, idx) => { + if (fullFrameData[dfd]) { + // New additions / updates to values of object + const additions = webSocketData[dfd] ? webSocketData[dfd] : {}; + // Append new values / updates to values of object + let updateValues = {...renderAll[dfd], ...additions} + // Create a new key-value pair from deltaFrame key and new/updated values + let newObj = {}; + newObj[dfd] = updateValues; + // Append new object to fullFrame data object without mutation of original one + renderAll = {...renderAll, ...newObj}; + } else { + let newObj = {}; + newObj[dfd] = webSocketData[dfd]; + // Append wsData object to fullFrame object and return it + renderAll = {...renderAll, ...newObj}; + } + }) + // Set fullFrame with update data from delta frame + setFullFrameData(renderAll); + return renderAll; + } else { + // Set full frame with full frame data + setFullFrameData(webSocketData); + return webSocketData; + } } // Fallback if wsData will not meet the condition requirements return fullFrameData; @@ -186,7 +194,7 @@ export default function InstancesContainer(props) { {t("InstancesContainer|Service")} - {t("InstancesContainer|Node")} + {t("InstancesContainer|Node ID")} {t("InstancesContainer|Name")} @@ -293,13 +301,17 @@ const RowContent = ({props, objKey, data, generateStatus}) => { const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); try { let response = await LMIORemoteControlAPI.post(`/instance/${id}`, body); - if (response.data.result != "OK") { + if (response.data.result != "Accepted") { throw new Error(`Something went wrong, failed to ${action} container`); } - props.app.addAlert("success", t("InstancesContainer|Instance action triggered successfully")); + props.app.addAlert("success", t("InstancesContainer|Instance action accepted successfully")); } catch(e) { console.error(e); - props.app.addAlert("warning", t("InstancesContainer|Instance action has not been triggered")); + if (e?.response?.data?.message) { + props.app.addAlert("warning", `${e?.response?.data?.message}`); + } else { + props.app.addAlert("warning", t("InstancesContainer|Instance action has been rejected")); + } } setIsSubmitting(false); } @@ -321,7 +333,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { {data[objKey]?.service} - {data[objKey]?.node} + {data[objKey]?.node_id} {data[objKey]?.name} From 60c25e2abfecfdcede1ce1284ea55b019bf7b4c6 Mon Sep 17 00:00:00 2001 From: fpesek Date: Thu, 27 Oct 2022 17:16:51 +0200 Subject: [PATCH 22/33] Update locales --- demo/public/locales/cs/translation.json | 6 +++--- demo/public/locales/en/translation.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index d2f88b72b..6b690358d 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -115,7 +115,7 @@ "Filter state": "Filtrovat stav", "Loading": "Načítání", "Service": "Služba", - "Node": "Node", + "Node ID": "Node ID", "Name": "Název", "Type": "Typ", "Version": "Verze", @@ -124,8 +124,8 @@ "Starting": "Spouští se", "Stopped": "Zastaveno", "Unknown": "Neznámo", - "Instance action triggered successfully": "Akce instance byla úspešně spuštěna", - "Instance action has not been triggered": "Akce instance nebyla spuštěna", + "Instance action accepted successfully": "Akce instance byla spuštěna", + "Instance action has been rejected": "Akce instance nebyla spuštěna", "Un-collapse": "Rozbalit", "Collapse": "Sbalit", "Start": "Spustit", diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index c24c83d12..f3450f99c 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -115,7 +115,7 @@ "Filter state": "Filter state", "Loading": "Loading", "Service": "Service", - "Node": "Node", + "Node ID": "Node ID", "Name": "Name", "Type": "Type", "Version": "Version", @@ -124,8 +124,8 @@ "Starting": "Starting", "Stopped": "Stopped", "Unknown": "Unknown", - "Instance action triggered successfully": "Instance action triggered successfully", - "Instance action has not been triggered": "Instance action has not been triggered", + "Instance action accepted successfully": "Instance action accepted successfully", + "Instance action has been rejected": "Instance action has been rejected", "Un-collapse": "Un-collapse", "Collapse": "Collapse", "Start": "Start", From 227a4e7322f81e5ae42c2fc81b461402cd78ce20 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 31 Oct 2022 09:53:44 +0100 Subject: [PATCH 23/33] Rename Instances to Services --- demo/public/locales/cs/translation.json | 8 +-- demo/public/locales/en/translation.json | 8 +-- .../ServicesContainers/ServicesContainer.js} | 62 +++++++++---------- .../components/ActionButton.js | 0 .../ServicesContainers/services.scss} | 2 +- .../index.js | 22 +++---- 6 files changed, 51 insertions(+), 51 deletions(-) rename src/modules/maintenance/{InstancesModule/InstancesContainers/InstancesContainer.js => ServicesModule/ServicesContainers/ServicesContainer.js} (86%) rename src/modules/maintenance/{InstancesModule/InstancesContainers => ServicesModule/ServicesContainers}/components/ActionButton.js (100%) rename src/modules/maintenance/{InstancesModule/InstancesContainers/instances.scss => ServicesModule/ServicesContainers/services.scss} (98%) rename src/modules/maintenance/{InstancesModule => ServicesModule}/index.js (67%) diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index 6b690358d..b722e09d3 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -108,10 +108,10 @@ "Only tar.gz files are allowed": "Povoleny jsou pouze soubory tar.gz", "Choose file": "Vyberte soubor" }, - "InstancesContainer": { + "ServicesContainer": { "Can't display data due to parsing error": "Nelze zobrazit data kvůli chybě v parsování", "Can't establish websocket connection, data can't be loaded": "Nepodařilo se navázat websocket spojení, data nelze načíst", - "Instances": "Instance", + "Services": "Služby", "Filter state": "Filtrovat stav", "Loading": "Načítání", "Service": "Služba", @@ -124,8 +124,8 @@ "Starting": "Spouští se", "Stopped": "Zastaveno", "Unknown": "Neznámo", - "Instance action accepted successfully": "Akce instance byla spuštěna", - "Instance action has been rejected": "Akce instance nebyla spuštěna", + "Service action accepted successfully": "Akce služby byla spuštěna", + "Service action has been rejected": "Akce služby nebyla spuštěna", "Un-collapse": "Rozbalit", "Collapse": "Sbalit", "Start": "Spustit", diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index f3450f99c..9f91ee234 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -108,10 +108,10 @@ "Only tar.gz files are allowed": "Only tar.gz files are allowed", "Choose file": "Choose file" }, - "InstancesContainer": { + "ServicesContainer": { "Can't display data due to parsing error": "Can't display data due to parsing error", "Can't establish websocket connection, data can't be loaded": "Can't establish websocket connection, data can't be loaded", - "Instances": "Instances", + "Services": "Services", "Filter state": "Filter state", "Loading": "Loading", "Service": "Service", @@ -124,8 +124,8 @@ "Starting": "Starting", "Stopped": "Stopped", "Unknown": "Unknown", - "Instance action accepted successfully": "Instance action accepted successfully", - "Instance action has been rejected": "Instance action has been rejected", + "Service action accepted successfully": "Service action accepted successfully", + "Service action has been rejected": "Service action has been rejected", "Un-collapse": "Un-collapse", "Collapse": "Collapse", "Start": "Start", diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js similarity index 86% rename from src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js rename to src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index c2cb97fde..6ad3d5752 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/InstancesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -12,7 +12,7 @@ import { CellContentLoader } from 'asab-webui'; import ActionButton from "./components/ActionButton"; -export default function InstancesContainer(props) { +export default function ServicesContainer(props) { const [fullFrameData, setFullFrameData] = useState({}); const [wsData, setWSData] = useState({}); @@ -143,14 +143,14 @@ export default function InstancesContainer(props) { } setError(false); } else { - setErrorMsg(t("InstancesContainer|Can't display data due to parsing error")); + setErrorMsg(t("ServicesContainer|Can't display data due to parsing error")); setError(true); } }; WSClient.onerror = (error) => { setLoading(false); - setErrorMsg(t("InstancesContainer|Can't establish websocket connection, data can't be loaded")); + setErrorMsg(t("ServicesContainer|Can't establish websocket connection, data can't be loaded")); setError(true); setTimeout(() => { reconnect(); @@ -164,17 +164,17 @@ export default function InstancesContainer(props) {
- {t("InstancesContainer|Instances")} + {t("ServicesContainer|Services")}
- + {(loading == true) ? - + : @@ -191,19 +191,19 @@ export default function InstancesContainer(props) { @@ -237,7 +237,7 @@ const DataRow = ({data, props}) => { */ const generateStatus = (status) => { if (status == undefined) { - return (
); + return (
); } if (typeof status === "string") { return statusTranslations(status); @@ -252,16 +252,16 @@ const DataRow = ({data, props}) => { const statusTranslations = (status) => { if (status.toLowerCase() === "running") { - return (
); + return (
); }; if (status.toLowerCase() === "starting") { - return (
); + return (
); }; if (status.toLowerCase() === "stopped") { - return (
); + return (
); }; if (status.toLowerCase() === "unknown") { - return (
); + return (
); }; return (
); } @@ -304,13 +304,13 @@ const RowContent = ({props, objKey, data, generateStatus}) => { if (response.data.result != "Accepted") { throw new Error(`Something went wrong, failed to ${action} container`); } - props.app.addAlert("success", t("InstancesContainer|Instance action accepted successfully")); + props.app.addAlert("success", t("ServicesContainer|Service action accepted successfully")); } catch(e) { console.error(e); if (e?.response?.data?.message) { props.app.addAlert("warning", `${e?.response?.data?.message}`); } else { - props.app.addAlert("warning", t("InstancesContainer|Instance action has been rejected")); + props.app.addAlert("warning", t("ServicesContainer|Service action has been rejected")); } } setIsSubmitting(false); @@ -322,9 +322,9 @@ const RowContent = ({props, objKey, data, generateStatus}) => {
- @@ -333,13 +330,10 @@ const RowContent = ({props, objKey, data, generateStatus}) => { {data[objKey]?.service} - - @@ -387,6 +386,13 @@ const RowContent = ({props, objKey, data, generateStatus}) => { {!collapseData &&
- {t("InstancesContainer|Service")} + {t("ServicesContainer|Service")} - {t("InstancesContainer|Node ID")} + {t("ServicesContainer|Node ID")} - {t("InstancesContainer|Name")} + {t("ServicesContainer|Name")} - {t("InstancesContainer|Type")} + {t("ServicesContainer|Type")} - {t("InstancesContainer|Version")} + {t("ServicesContainer|Version")}
{collapseData ? - {setCollapseData(false)}}> + {setCollapseData(false)}}> : - {setCollapseData(true)}}> + {setCollapseData(true)}}> } {generateStatus(data[objKey]?.state ? data[objKey].state : undefined)}
@@ -348,7 +348,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => {
{ disabled={isSubmitting == true} /> { disabled={isSubmitting == true} /> { disabled={isSubmitting == true} /> {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} @@ -395,14 +395,14 @@ const RowContent = ({props, objKey, data, generateStatus}) => {
{data[objKey]?.returncode?.toString() ?
- {t("InstancesContainer|Return code")}: {data[objKey]?.returncode?.toString()} + {t("ServicesContainer|Return code")}: {data[objKey]?.returncode?.toString()}
: null } {data[objKey]?.error ?
- {t("InstancesContainer|Error")}: {data[objKey]?.error?.toString()} + {t("ServicesContainer|Error")}: {data[objKey]?.error?.toString()}
: null @@ -410,7 +410,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { {data[objKey]?.exception ?
- {t("InstancesContainer|Exception")}: + {t("ServicesContainer|Exception")}: { {data[objKey]?.console ?
- {t("InstancesContainer|Console")}: + {t("ServicesContainer|Console")}: { {data[objKey]?.detail ? : null @@ -454,7 +454,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { {data[objKey]?.advertised_data ? : null diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js b/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js similarity index 100% rename from src/modules/maintenance/InstancesModule/InstancesContainers/components/ActionButton.js rename to src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js diff --git a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss similarity index 98% rename from src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss rename to src/modules/maintenance/ServicesModule/ServicesContainers/services.scss index 8f0a493c2..5c37ee61c 100644 --- a/src/modules/maintenance/InstancesModule/InstancesContainers/instances.scss +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss @@ -9,7 +9,7 @@ /* card body styles */ -.instances-body { +.services-body { overflow: auto; padding-top: 0 !important; padding-bottom: 0 !important; diff --git a/src/modules/maintenance/InstancesModule/index.js b/src/modules/maintenance/ServicesModule/index.js similarity index 67% rename from src/modules/maintenance/InstancesModule/index.js rename to src/modules/maintenance/ServicesModule/index.js index 298fe0a50..43df0b9b0 100644 --- a/src/modules/maintenance/InstancesModule/index.js +++ b/src/modules/maintenance/ServicesModule/index.js @@ -1,18 +1,18 @@ import React, { Component } from 'react'; import Module from 'asab-webui/abc/Module'; -import InstancesContainer from "./InstancesContainers/InstancesContainer"; +import ServicesContainer from "./ServicesContainers/ServicesContainer"; -import "./InstancesContainers/instances.scss"; +import "./ServicesContainers/services.scss"; -export default class InstancesModule extends Module { +export default class ServicesModule extends Module { constructor(app, name) { - super(app, "ASABInstancesModule"); + super(app, "ASABServicesModule"); app.Router.addRoute({ - path: "/instances", + path: "/services", exact: true, - name: "Instances", - component: InstancesContainer, + name: "Services", + component: ServicesContainer, }); // Check presence of Maintenance item in sidebar @@ -22,8 +22,8 @@ export default class InstancesModule extends Module { // If Maintenance present, then append Microservices as a Maintenance subitem if (itm?.name == "Maintenance") { itm.children.push({ - name: "Instances", - url: "/instances", + name: "Services", + url: "/services", icon: "cil-list" }); isMaintenancePresent = true; @@ -37,8 +37,8 @@ export default class InstancesModule extends Module { icon: "cil-apps-settings", children: [ { - name: "Instances", - url: "/instances", + name: "Services", + url: "/services", icon: "cil-list" } ] From 8076460e044c7377c288866aee39bc436ebc156f Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 31 Oct 2022 11:08:47 +0100 Subject: [PATCH 24/33] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ecbc295..f78e56a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Release Candidate +### Features + +- Refactor Microservices container to Services and implement websocket connection (INDIGO Sprint 221014, [!363](https://github.com/TeskaLabs/asab-webui/pull/363)) + ## v22.42 ### Refactoring From dd7d1dbec56b0f0021befae6854131cbd5fac96d Mon Sep 17 00:00:00 2001 From: fpesek Date: Tue, 8 Nov 2022 10:12:43 +0100 Subject: [PATCH 25/33] Update classname for status circle --- .../ServicesContainers/ServicesContainer.js | 12 ++++++------ .../ServicesModule/ServicesContainers/services.scss | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index 6ad3d5752..6da364f0b 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -237,7 +237,7 @@ const DataRow = ({data, props}) => { */ const generateStatus = (status) => { if (status == undefined) { - return (
); + return (
); } if (typeof status === "string") { return statusTranslations(status); @@ -252,18 +252,18 @@ const DataRow = ({data, props}) => { const statusTranslations = (status) => { if (status.toLowerCase() === "running") { - return (
); + return (
); }; if (status.toLowerCase() === "starting") { - return (
); + return (
); }; if (status.toLowerCase() === "stopped") { - return (
); + return (
); }; if (status.toLowerCase() === "unknown") { - return (
); + return (
); }; - return (
); + return (
); } return( diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss index 5c37ee61c..d28715c56 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss @@ -17,7 +17,7 @@ /* status indicator styles */ -.status-circle { +.service-status-circle { margin-top: 0.25rem; width: 0.8rem; height: 0.8rem; @@ -25,15 +25,15 @@ border-radius: 50% } -.status-running { +.service-status-running { background: $light-green; } -.status-starting { +.service-status-starting { background: $warning; } -.status-stopped { +.service-status-stopped { background: $danger; } From 8feb2d6715d972c70a28275137238a8371b16108 Mon Sep 17 00:00:00 2001 From: fpesek Date: Tue, 8 Nov 2022 15:19:47 +0100 Subject: [PATCH 26/33] Bugfix on action button id tooltip --- .../ServicesContainers/ServicesContainer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index 6da364f0b..21546f37a 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -349,7 +349,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { { /> { /> {setAction("restart", data[objKey]?.instance_id), setIsSubmitting(true)}} @@ -379,7 +379,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { /> {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-arrow-thick-from-bottom" From 12157ff560105963608cb66d7664739b9f66d1d7 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 14 Nov 2022 10:19:35 +0100 Subject: [PATCH 27/33] Add full buttons, remove type column, change button icons, add monochrome text style to node id and name column values --- .../ServicesContainers/ServicesContainer.js | 21 ++++++------------- .../ServicesContainers/services.scss | 12 ----------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index 21546f37a..c8bb0563a 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -199,9 +199,6 @@ export default function ServicesContainer(props) {
{t("ServicesContainer|Name")} - {t("ServicesContainer|Type")} - {t("ServicesContainer|Version")} - {data[objKey]?.node_id} + {data[objKey]?.node_id?.toString()} - {data[objKey]?.name} - - {data[objKey]?.type} + {data[objKey]?.name?.toString()} {data[objKey]?.advertised_data?.version ? data[objKey]?.advertised_data?.version : data[objKey]?.version ? data[objKey]?.version : "N/A"} @@ -354,7 +348,6 @@ const RowContent = ({props, objKey, data, generateStatus}) => { color="primary" icon="cil-media-play" onClick={() => {setAction("start", data[objKey]?.instance_id), setIsSubmitting(true)}} - outline disabled={isSubmitting == true} /> { id={`stop-${objKey.replace(/[^\w\s]/gi, '-')}`} className="action-button" color="danger" - outline onClick={() => {setAction("stop", data[objKey]?.instance_id), setIsSubmitting(true)}} - icon="cil-ban" + icon="cil-media-stop" disabled={isSubmitting == true} /> { id={`restart-${objKey.replace(/[^\w\s]/gi, '-')}`} className="action-button" color="secondary" + outline onClick={() => {setAction("restart", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-reload" - outline disabled={isSubmitting == true} /> {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} - icon="cil-arrow-thick-from-bottom" - outline + icon="cil-media-eject" disabled={isSubmitting == true} /> diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss index d28715c56..d7e447a9c 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss @@ -98,15 +98,3 @@ .collapsed-span { padding-right: 0.5em; } - -.action-button { - border-right: 1px solid $secondary; -} - -.action-button:hover { - border-right: 1px solid $secondary; -} - -.action-button:disabled { - border-right: 1px solid $secondary; -} From 489bc6c2fe3bf0072f6bb1ea6773abbf71f3b2e2 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 14 Nov 2022 10:19:51 +0100 Subject: [PATCH 28/33] Update locales --- demo/public/locales/cs/translation.json | 1 - demo/public/locales/en/translation.json | 1 - 2 files changed, 2 deletions(-) diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index b722e09d3..1ad500480 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -117,7 +117,6 @@ "Service": "Služba", "Node ID": "Node ID", "Name": "Název", - "Type": "Typ", "Version": "Verze", "Not defined": "Neurčeno", "Running": "Běží", diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index 9f91ee234..caa3e41e7 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -117,7 +117,6 @@ "Service": "Service", "Node ID": "Node ID", "Name": "Name", - "Type": "Type", "Version": "Version", "Not defined": "Not defined", "Running": "Running", From 15d74e52b26888ee871fb28b8c4ccf790111aff8 Mon Sep 17 00:00:00 2001 From: fpesek Date: Mon, 14 Nov 2022 10:23:31 +0100 Subject: [PATCH 29/33] Update docu for services --- doc/{asab-instances.md => asab-services.md} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename doc/{asab-instances.md => asab-services.md} (63%) diff --git a/doc/asab-instances.md b/doc/asab-services.md similarity index 63% rename from doc/asab-instances.md rename to doc/asab-services.md index 8f0ae131e..7f5226cee 100644 --- a/doc/asab-instances.md +++ b/doc/asab-services.md @@ -1,10 +1,10 @@ -# ASAB Instances +# ASAB Services -ASAB WebUI Instances is a page with a list of available instances. It use a websocket connection, so the data are propagated realtime. +ASAB WebUI Services is a page with a list of available instances. It use a websocket connection, so the data are propagated realtime. ## Setup -In `config` file, define ASAB Instances as a service: +In `config` file, define ASAB Services as a service: ``` module.exports = { @@ -26,15 +26,15 @@ module.exports = { } ``` -In the top-level `index.js` of your ASAB UI application, load the ASAB microservices module +In the top-level `index.js` of your ASAB UI application, load the ASAB services module ``` const modules = []; ... -import ASABInstancesModule from 'asab-webui/modules/maintenance/InstancesModule'; -modules.push(ASABInstancesModule); +import ASABServicesModule from 'asab-webui/modules/maintenance/ServicesModule'; +modules.push(ASABServicesModule); ... From 65baba08d42272c471d995746d4b46e3ba0a5e24 Mon Sep 17 00:00:00 2001 From: Pe5h4 Date: Mon, 14 Nov 2022 11:24:52 +0100 Subject: [PATCH 30/33] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b63ed7b1d..1f4629075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Refactor Microservices container to Services and implement websocket connection (INDIGO Sprint 221014, [!363](https://github.com/TeskaLabs/asab-webui/pull/363)) +- Refactor Microservices container to Services and implement websocket connection (INDIGO Sprint 221111, [!363](https://github.com/TeskaLabs/asab-webui/pull/363)) ### Bugfixes From 7ae61eae6b60f87d21963233ce963b5f800019e5 Mon Sep 17 00:00:00 2001 From: Pe5h4 Date: Mon, 14 Nov 2022 11:28:29 +0100 Subject: [PATCH 31/33] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4629075..4d10f99e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Release Candidate +## v22.46 + ### Features - Refactor Microservices container to Services and implement websocket connection (INDIGO Sprint 221111, [!363](https://github.com/TeskaLabs/asab-webui/pull/363)) From 0130b0c213ba4827d6ba7ec6009e96a64444633b Mon Sep 17 00:00:00 2001 From: Pe5h4 Date: Mon, 14 Nov 2022 11:47:10 +0100 Subject: [PATCH 32/33] Change color of buttons --- .../ServicesModule/ServicesContainers/ServicesContainer.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index c8bb0563a..b1d784095 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -345,7 +345,8 @@ const RowContent = ({props, objKey, data, generateStatus}) => { label={t("ServicesContainer|Start")} id={`start-${objKey.replace(/[^\w\s]/gi, '-')}`} className="action-button" - color="primary" + color="secondary" + outline icon="cil-media-play" onClick={() => {setAction("start", data[objKey]?.instance_id), setIsSubmitting(true)}} disabled={isSubmitting == true} @@ -354,7 +355,8 @@ const RowContent = ({props, objKey, data, generateStatus}) => { label={t("ServicesContainer|Stop")} id={`stop-${objKey.replace(/[^\w\s]/gi, '-')}`} className="action-button" - color="danger" + color="secondary" + outline onClick={() => {setAction("stop", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-media-stop" disabled={isSubmitting == true} @@ -373,6 +375,7 @@ const RowContent = ({props, objKey, data, generateStatus}) => { label={t("ServicesContainer|Up")} id={`up-${objKey.replace(/[^\w\s]/gi, '-')}`} color="secondary" + outline onClick={() => {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} icon="cil-media-eject" disabled={isSubmitting == true} From 3d17f9f5fac11a96d249d0a767a0a69ae18c87b3 Mon Sep 17 00:00:00 2001 From: Pe5h4 Date: Mon, 14 Nov 2022 14:26:12 +0100 Subject: [PATCH 33/33] Move Type to collapsed content --- demo/public/locales/cs/translation.json | 1 + demo/public/locales/en/translation.json | 1 + .../ServicesContainers/ServicesContainer.js | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index 1ad500480..b722e09d3 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -117,6 +117,7 @@ "Service": "Služba", "Node ID": "Node ID", "Name": "Název", + "Type": "Typ", "Version": "Verze", "Not defined": "Neurčeno", "Running": "Běží", diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index caa3e41e7..9f91ee234 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -117,6 +117,7 @@ "Service": "Service", "Node ID": "Node ID", "Name": "Name", + "Type": "Type", "Version": "Version", "Not defined": "Not defined", "Running": "Running", diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js index b1d784095..5e04eb289 100644 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js @@ -184,7 +184,6 @@ export default function ServicesContainer(props) {
+ {data[objKey]?.type ? +
+ {t("ServicesContainer|Type")}: {data[objKey]?.type?.toString()} +
+ : + null + } {data[objKey]?.returncode?.toString() ?
{t("ServicesContainer|Return code")}: {data[objKey]?.returncode?.toString()}