diff --git a/CHANGELOG.md b/CHANGELOG.md
index e803fe3e9..4d10f99e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,17 @@
## 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))
+
### Bugfixes
- Change const variable customCellStyle to let variable in DataTable (INDIGO Sprint 221031, [!374](https://github.com/TeskaLabs/asab-webui/pull/374))
+
## v22.42
### Refactoring
diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json
index 9c800bf26..b722e09d3 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",
+ "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",
+ "Services": "Služby",
+ "Filter state": "Filtrovat stav",
+ "Loading": "Načítání",
+ "Service": "Služba",
+ "Node ID": "Node ID",
+ "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",
+ "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",
+ "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..9f91ee234 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",
+ "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",
+ "Services": "Services",
+ "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 ID": "Node ID",
+ "Name": "Name",
+ "Type": "Type",
"Version": "Version",
- "Attention": "Attention"
+ "Not defined": "Not defined",
+ "Running": "Running",
+ "Starting": "Starting",
+ "Stopped": "Stopped",
+ "Unknown": "Unknown",
+ "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",
+ "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-microservices.md
deleted file mode 100644
index 486671cb7..000000000
--- a/doc/asab-microservices.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# ASAB Microservices
-
-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.
-
-## Setup
-
-In `config` file, define ASAB Microservices as a service:
-
-```
-module.exports = {
- app: {
-
- ...
-
- },
- webpackDevServer: {
- port: 3000,
- proxy: {
- '/api/lmio_correlator_builder': {
- target: 'http://localhost:8086',
- pathRewrite: {'^/api/lmio_correlator_builder' : ''},
- ws: true
- },
- }
- }
-}
-```
-
-In the top-level `index.js` of your ASAB UI application, load the ASAB microservices module
-
-```
-const modules = [];
-
-...
-
-import ASABMicroservicesModule from 'asab-webui/modules/maintenance/MicroservicesModule';
-modules.push(ASABMicroservicesModule);
-
-...
-
-ReactDOM.render((
-
-
-
-), document.getElementById('app'));
-```
-
-The module will be displayed as a subitem of `Maintenance` in the sidebar navigation.
diff --git a/doc/asab-services.md b/doc/asab-services.md
new file mode 100644
index 000000000..7f5226cee
--- /dev/null
+++ b/doc/asab-services.md
@@ -0,0 +1,48 @@
+# ASAB Services
+
+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 Services as a service:
+
+```
+module.exports = {
+ app: {
+
+ ...
+
+ },
+ webpackDevServer: {
+ port: 3000,
+ proxy: {
+ '/api/lmio_remote_control': {
+ target: 'http://localhost:8086',
+ ws: true,
+ pathRewrite: {'^/api/lmio_remote_control' : ''}
+ },
+ }
+ }
+}
+```
+
+In the top-level `index.js` of your ASAB UI application, load the ASAB services module
+
+```
+const modules = [];
+
+...
+
+import ASABServicesModule from 'asab-webui/modules/maintenance/ServicesModule';
+modules.push(ASABServicesModule);
+
+...
+
+ReactDOM.render((
+
+
+
+), document.getElementById('app'));
+```
+
+The module will be displayed as a subitem of `Maintenance` in the sidebar navigation.
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) =>
- {header} |
- )}
-
-
-
- {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 ? (
- {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
diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js
deleted file mode 100644
index e36a31557..000000000
--- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/MicroservicesContainer.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-
-import { Container } from 'reactstrap';
-
-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 { t } = useTranslation();
-
- const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control');
-
- 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) => {
- 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;
- }
- }
-
- return (
-
-
-
- )
-}
diff --git a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss b/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss
deleted file mode 100644
index d19f70ffc..000000000
--- a/src/modules/maintenance/MicroservicesModule/MicroservicesContainers/microservices.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.svcs-container {
- td:nth-child(3n) {
- font-family: monospace;
- }
-}
diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js
new file mode 100644
index 000000000..5e04eb289
--- /dev/null
+++ b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js
@@ -0,0 +1,553 @@
+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, CardHeader, Table,
+ InputGroup, InputGroupText, Input, InputGroupAddon,
+ ButtonGroup, Button
+} from 'reactstrap';
+
+import { CellContentLoader } from 'asab-webui';
+
+import ActionButton from "./components/ActionButton";
+
+export default function ServicesContainer(props) {
+
+ const [fullFrameData, setFullFrameData] = useState({});
+ const [wsData, setWSData] = useState({});
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ const [filter, setFilter] = useState("");
+
+ const theme = useSelector(state => state.theme);
+
+ const { t } = useTranslation();
+
+ // Set up websocket connection
+ let wsSubPath = '/ws';
+ 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;
+
+ if (WSUrl != undefined) {
+ reconnect();
+ }
+
+ return () => {
+ if (WSClient != null) {
+ try {
+ WSClient.close();
+ } catch (e) {
+ console.log("Ignored exception: ", e)
+ }
+ }
+
+ isMounted.current = false;
+ }
+ }, []);
+
+
+ // Use memo for data rendering (due to expensive caluclations)
+ const data = useMemo(() => {
+ let webSocketData = wsData.data;
+ // Render ws data
+ 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;
+ }, [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) {
+ try {
+ WSClient.close();
+ } catch (e) {
+ console.log("Ignored exception: ", e)
+ }
+ }
+
+ if (isMounted.current === false) return;
+
+ WSClient = props.app.createWebSocket(serviceName, wsSubPath);
+
+ // TODO: remove onopen
+ WSClient.onopen = () => {
+ console.log('ws connection open');
+ }
+
+ WSClient.onmessage = (message) => {
+ setLoading(false);
+ if (IsJsonString(message.data) == true) {
+ let retrievedData = JSON.parse(message.data);
+ if (retrievedData && Object.keys(retrievedData)) {
+ // Set websocket data
+ setWSData(retrievedData);
+ }
+ setError(false);
+ } else {
+ setErrorMsg(t("ServicesContainer|Can't display data due to parsing error"));
+ setError(true);
+ }
+ };
+
+ WSClient.onerror = (error) => {
+ setLoading(false);
+ setErrorMsg(t("ServicesContainer|Can't establish websocket connection, data can't be loaded"));
+ setError(true);
+ setTimeout(() => {
+ reconnect();
+ }, 3000, this);
+ };
+ }
+
+
+ return (
+
+
+
+
+ {t("ServicesContainer|Services")}
+
+
+
+
+ {(loading == true) ?
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ {t("ServicesContainer|Service")}
+ |
+
+ {t("ServicesContainer|Node ID")}
+ |
+
+ {t("ServicesContainer|Name")}
+ |
+
+ {t("ServicesContainer|Version")}
+ |
+
+ |
+
+
+
+ {(error == true) ?
+
+ {errorMsg} |
+
+ :
+
+ }
+
+
+ }
+
+
+
+ )
+}
+
+// Method to render table row with data
+const DataRow = ({data, props}) => {
+ const { t } = useTranslation();
+
+ // 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((objKey) => (
+
+ ))
+ )
+}
+
+// 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(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ 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 != "Accepted") {
+ throw new Error(`Something went wrong, failed to ${action} container`);
+ }
+ 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("ServicesContainer|Service action has been rejected"));
+ }
+ }
+ setIsSubmitting(false);
+ }
+
+ return(
+ <>
+
+
+
+ {collapseData ?
+ {setCollapseData(false)}}>
+ :
+ {setCollapseData(true)}}>
+ }
+ {generateStatus(data[objKey]?.state ? data[objKey].state : undefined)}
+
+ |
+
+ {data[objKey]?.service}
+ |
+
+ {data[objKey]?.node_id?.toString()}
+ |
+
+ {data[objKey]?.name?.toString()}
+ |
+
+ {data[objKey]?.advertised_data?.version ? data[objKey]?.advertised_data?.version : data[objKey]?.version ? data[objKey]?.version : "N/A"}
+ |
+
+
+
+ {setAction("start", data[objKey]?.instance_id), setIsSubmitting(true)}}
+ disabled={isSubmitting == true}
+ />
+ {setAction("stop", data[objKey]?.instance_id), setIsSubmitting(true)}}
+ icon="cil-media-stop"
+ disabled={isSubmitting == true}
+ />
+ {setAction("restart", data[objKey]?.instance_id), setIsSubmitting(true)}}
+ icon="cil-reload"
+ disabled={isSubmitting == true}
+ />
+ {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}}
+ icon="cil-media-eject"
+ disabled={isSubmitting == true}
+ />
+
+
+ |
+
+ {!collapseData &&
+
+
+ {data[objKey]?.type ?
+
+ {t("ServicesContainer|Type")}: {data[objKey]?.type?.toString()}
+
+ :
+ null
+ }
+ {data[objKey]?.returncode?.toString() ?
+
+ {t("ServicesContainer|Return code")}: {data[objKey]?.returncode?.toString()}
+
+ :
+ null
+ }
+ {data[objKey]?.error ?
+
+ {t("ServicesContainer|Error")}: {data[objKey]?.error?.toString()}
+
+ :
+ null
+ }
+ {data[objKey]?.exception ?
+
+
+ {t("ServicesContainer|Exception")}:
+
+
+
+ :
+ null
+ }
+ {data[objKey]?.console ?
+
+
+ {t("ServicesContainer|Console")}:
+
+
+
+ :
+ null
+ }
+ {data[objKey]?.detail ?
+
+ :
+ null
+ }
+ {data[objKey]?.advertised_data ?
+
+ :
+ null
+ }
+ |
+
+ }
+ >
+ )
+}
+
+// Method to display collapsed table
+const CollapsedTable = ({obj, title}) => {
+ const theme = useSelector(state => state.theme);
+
+ return(
+
+
+
+ {title} |
+
+
+
+ {Object.keys(obj).length != 0 && Object.entries(obj).map((itms, idx) => {
+ return(
+
+
+
+ {itms[0] && itms[0].toString()}
+
+ |
+
+ {itms[1] ?
+ (typeof itms[1] == "object") && (Object.keys(itms[1]).length > 0) ?
+
+ :
+ {itms[1] && itms[1].toString()}
+ : "-"}
+ |
+
+ )
+ })}
+
+
+ )
+}
+
+
+// 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 {
+ JSON.parse(str);
+ } catch (e) {
+ return false;
+ }
+ return true;
+}
diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js b/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js
new file mode 100644
index 000000000..8f7f73456
--- /dev/null
+++ b/src/modules/maintenance/ServicesModule/ServicesContainers/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/ServicesModule/ServicesContainers/services.scss b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss
new file mode 100644
index 000000000..d7e447a9c
--- /dev/null
+++ b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss
@@ -0,0 +1,100 @@
+@import "~asab-webui/styles/constants/index.scss";
+
+/* container styles */
+
+.svcs-container {
+ height: 100%;
+
+}
+
+/* card body styles */
+
+.services-body {
+ overflow: auto;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+/* status indicator styles */
+
+.service-status-circle {
+ margin-top: 0.25rem;
+ width: 0.8rem;
+ height: 0.8rem;
+ background: $secondary;
+ border-radius: 50%
+}
+
+.service-status-running {
+ background: $light-green;
+}
+
+.service-status-starting {
+ background: $warning;
+}
+
+.service-status-stopped {
+ background: $danger;
+}
+
+/* table styles */
+
+.td-style {
+ text-align: center;
+ 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 {
+ padding-right: 0.75rem;
+ padding-top: 0.15rem;
+ color: $primary;
+ 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;
+}
diff --git a/src/modules/maintenance/MicroservicesModule/index.js b/src/modules/maintenance/ServicesModule/index.js
similarity index 55%
rename from src/modules/maintenance/MicroservicesModule/index.js
rename to src/modules/maintenance/ServicesModule/index.js
index 95f1e9e5d..43df0b9b0 100644
--- a/src/modules/maintenance/MicroservicesModule/index.js
+++ b/src/modules/maintenance/ServicesModule/index.js
@@ -1,26 +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 ServicesContainer from "./ServicesContainers/ServicesContainer";
-export default class MicroservicesModule extends Module {
+import "./ServicesContainers/services.scss";
+
+export default class ServicesModule extends Module {
constructor(app, name) {
- super(app, "ASABMicroservicesModule");
+ super(app, "ASABServicesModule");
app.Router.addRoute({
- path: "/microservices/svcs",
+ path: "/services",
exact: true,
- name: "Microservices",
- component: MicroservicesContainer,
+ name: "Services",
+ component: ServicesContainer,
});
- 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;
@@ -28,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: "Services",
+ url: "/services",
icon: "cil-list"
});
isMaintenancePresent = true;
@@ -43,8 +37,8 @@ export default class MicroservicesModule extends Module {
icon: "cil-apps-settings",
children: [
{
- name: "Microservices",
- url: "/microservices/svcs",
+ name: "Services",
+ url: "/services",
icon: "cil-list"
}
]