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) => - - )} - - - - {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 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) ? + + : + + + + + + + + + + + + + + + + + + + + + {(error == true) ? + + + + : + + } + +
+ + {t("ServicesContainer|Service")} + + {t("ServicesContainer|Node ID")} + + {t("ServicesContainer|Name")} + + {t("ServicesContainer|Version")} + +
{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( + + + + + + + + {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()} + : "-"} +
+ ) +} + + +// 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" } ]