mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 17:32:20 +00:00
[WIFI-10714] System page fix for RRM and other endpoints witthout subsystems
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.7.0(3)",
|
||||
"version": "2.7.0(4)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.7.0(3)",
|
||||
"version": "2.7.0(4)",
|
||||
"dependencies": {
|
||||
"@coreui/coreui": "^3.4.0",
|
||||
"@coreui/icons": "^2.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.7.0(3)",
|
||||
"version": "2.7.0(4)",
|
||||
"dependencies": {
|
||||
"@coreui/coreui": "^3.4.0",
|
||||
"@coreui/icons": "^2.0.1",
|
||||
|
||||
32
src/components/FormattedDate/index.js
Normal file
32
src/components/FormattedDate/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CPopover } from '@coreui/react';
|
||||
import { formatDaysAgo, prettyDate } from 'utils/helper';
|
||||
|
||||
const FormattedDate = ({ date, size }) => {
|
||||
if (size === 'lg') {
|
||||
return (
|
||||
<CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}>
|
||||
<h2 className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</h2>
|
||||
</CPopover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CPopover content={prettyDate(date)} advancedOptions={{ animation: false }}>
|
||||
<span className="d-inline-block">{date === 0 ? '-' : formatDaysAgo(date)}</span>
|
||||
</CPopover>
|
||||
);
|
||||
};
|
||||
|
||||
FormattedDate.propTypes = {
|
||||
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
FormattedDate.defaultProps = {
|
||||
date: 0,
|
||||
size: 'md',
|
||||
};
|
||||
|
||||
export default FormattedDate;
|
||||
17
src/hooks/useToggle/index.js
Normal file
17
src/hooks/useToggle/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const useToggle = (initialState) => {
|
||||
const [value, setValue] = useState(initialState);
|
||||
|
||||
return [
|
||||
value,
|
||||
() => {
|
||||
setValue(!value);
|
||||
},
|
||||
(newValue) => {
|
||||
setValue(newValue);
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default useToggle;
|
||||
189
src/pages/SystemPage/ApiStatusCard/index.js
Normal file
189
src/pages/SystemPage/ApiStatusCard/index.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
CCard,
|
||||
CCardBody,
|
||||
CCardHeader,
|
||||
CRow,
|
||||
CCol,
|
||||
CButton,
|
||||
CPopover,
|
||||
CModal,
|
||||
CModalBody,
|
||||
CModalHeader,
|
||||
CModalTitle,
|
||||
CDataTable,
|
||||
} from '@coreui/react';
|
||||
import Select from 'react-select';
|
||||
import CIcon from '@coreui/icons-react';
|
||||
import { cilSync, cilX } from '@coreui/icons';
|
||||
import { prettyDate } from 'utils/helper';
|
||||
import useToggle from 'hooks/useToggle';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
|
||||
const ApiStatusCard = ({ t, info, reload }) => {
|
||||
const [types, setTypes] = useState([]);
|
||||
const [showCerts, toggleCerts] = useToggle();
|
||||
|
||||
const submit = () => {
|
||||
reload(
|
||||
types.map((v) => v.value),
|
||||
info.endpoint,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CCard>
|
||||
<CCardHeader className="dark-header">
|
||||
<div style={{ fontWeight: '600' }} className=" text-value-lg float-left">
|
||||
{info.title}
|
||||
</div>
|
||||
</CCardHeader>
|
||||
<CCardBody>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('common.endpoint')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.endpoint}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('system.hostname')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.hostname}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('system.os')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.os}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('system.processors')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.processors}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('common.start')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">
|
||||
{info.start ? <FormattedDate date={info.start} /> : t('common.unknown')}
|
||||
</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('status.uptime')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.uptime}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('footer.version')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">{info.version}</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4">
|
||||
<div block="true">{t('common.certificates')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">
|
||||
{info.certificates?.length > 0 ? (
|
||||
<CButton className="ml-0 pl-0 py-0" color="link" onClick={toggleCerts}>
|
||||
{t('common.details')} ({info.certificates.length})
|
||||
</CButton>
|
||||
) : (
|
||||
<div>{t('common.unknown')}</div>
|
||||
)}
|
||||
</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
<CRow>
|
||||
<CCol sm="4" className="pt-1">
|
||||
<div block="true">{t('system.reload_subsystems')}:</div>
|
||||
</CCol>
|
||||
<CCol>
|
||||
<div block="true">
|
||||
{info.subsystems.length === 0 ? (
|
||||
t('common.unknown')
|
||||
) : (
|
||||
<div>
|
||||
<div className="float-left" style={{ width: '85%' }}>
|
||||
<Select
|
||||
isMulti
|
||||
closeMenuOnSelect={false}
|
||||
name="Subsystems"
|
||||
options={info.subsystems.map((sys) => ({ value: sys, label: sys }))}
|
||||
onChange={setTypes}
|
||||
value={types}
|
||||
className="basic-multi-select"
|
||||
classNamePrefix="select"
|
||||
/>
|
||||
</div>
|
||||
<div className="float-left text-right" style={{ width: '15%' }}>
|
||||
<CPopover content={t('system.reload')}>
|
||||
<CButton
|
||||
color="primary"
|
||||
variant="outline"
|
||||
onClick={submit}
|
||||
disabled={types.length === 0}
|
||||
>
|
||||
<CIcon content={cilSync} />
|
||||
</CButton>
|
||||
</CPopover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CCol>
|
||||
</CRow>
|
||||
</CCardBody>
|
||||
<CModal size="lg" show={showCerts} onClose={toggleCerts}>
|
||||
<CModalHeader className="p-1">
|
||||
<CModalTitle className="pl-1 pt-1">{t('common.certificates')}</CModalTitle>
|
||||
<div className="text-right">
|
||||
<CPopover content={t('common.close')}>
|
||||
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleCerts}>
|
||||
<CIcon content={cilX} />
|
||||
</CButton>
|
||||
</CPopover>
|
||||
</div>
|
||||
</CModalHeader>
|
||||
<CModalBody>
|
||||
<CDataTable
|
||||
addTableClasses="table-sm"
|
||||
border
|
||||
items={info?.certificates.map((cert) => ({
|
||||
...cert,
|
||||
expiresOn: prettyDate(cert.expiresOn),
|
||||
}))}
|
||||
/>
|
||||
</CModalBody>
|
||||
</CModal>
|
||||
</CCard>
|
||||
);
|
||||
};
|
||||
|
||||
ApiStatusCard.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
info: PropTypes.instanceOf(Object).isRequired,
|
||||
reload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(ApiStatusCard);
|
||||
@@ -1,22 +1,135 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { v4 as createUuid } from 'uuid';
|
||||
import { CRow, CCol } from '@coreui/react';
|
||||
import { useAuth, useToast } from 'ucentral-libs';
|
||||
import { secondsToDetailed } from 'utils/helper';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SystemPage as Page, useToast, useAuth } from 'ucentral-libs';
|
||||
import axiosInstance from 'utils/axiosInstance';
|
||||
import ApiStatusCard from './ApiStatusCard';
|
||||
|
||||
const SystemPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentToken, endpoints } = useAuth();
|
||||
const { addToast } = useToast();
|
||||
const [endpointsInfo, setEndpointsInfo] = useState([]);
|
||||
|
||||
const getSystemInfo = async (key, endpoint) => {
|
||||
let systemInfo = {
|
||||
title: key,
|
||||
endpoint,
|
||||
hostname: t('common.unknown'),
|
||||
os: t('common.unknown'),
|
||||
processors: t('common.unknown'),
|
||||
uptime: t('common.unknown'),
|
||||
version: t('common.unknown'),
|
||||
certificates: [],
|
||||
subsystems: [],
|
||||
};
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${currentToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
await axiosInstance
|
||||
.get(`${endpoint}/api/v1/system?command=info`, options)
|
||||
.then((newInfo) => {
|
||||
systemInfo = { ...systemInfo, ...newInfo.data };
|
||||
systemInfo.uptime = secondsToDetailed(
|
||||
newInfo.data.uptime,
|
||||
t('common.day'),
|
||||
t('common.days'),
|
||||
t('common.hour'),
|
||||
t('common.hours'),
|
||||
t('common.minute'),
|
||||
t('common.minutes'),
|
||||
t('common.second'),
|
||||
t('common.seconds'),
|
||||
);
|
||||
systemInfo.start = newInfo.data.start;
|
||||
})
|
||||
.catch(() => {});
|
||||
await axiosInstance
|
||||
.post(`${endpoint}/api/v1/system`, { command: 'getsubsystemnames' }, options)
|
||||
.then((newSubs) => {
|
||||
systemInfo.subsystems = newSubs.data.list.sort((a, b) => {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return systemInfo;
|
||||
};
|
||||
|
||||
const reload = (subsystems, endpoint) => {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${currentToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
command: 'reload',
|
||||
subsystems,
|
||||
};
|
||||
|
||||
axiosInstance
|
||||
.post(`${endpoint}/api/v1/system?command=info`, parameters, options)
|
||||
.then(() => {
|
||||
addToast({
|
||||
title: t('common.success'),
|
||||
body: t('system.success_reload'),
|
||||
color: 'success',
|
||||
autohide: true,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
addToast({
|
||||
title: t('common.error'),
|
||||
body: t('system.error_reloading', { error: e.response?.data?.ErrorDescription }),
|
||||
color: 'danger',
|
||||
autohide: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getAllInfo = async () => {
|
||||
const promises = [];
|
||||
|
||||
for (const [key, value] of Object.entries(endpoints)) {
|
||||
promises.push(getSystemInfo(key, value));
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(promises);
|
||||
setEndpointsInfo(results);
|
||||
} catch (e) {
|
||||
addToast({
|
||||
title: t('common.error'),
|
||||
body: t('system.error_fetching'),
|
||||
color: 'danger',
|
||||
autohide: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getAllInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page
|
||||
t={t}
|
||||
currentToken={currentToken}
|
||||
endpoints={endpoints}
|
||||
addToast={addToast}
|
||||
axiosInstance={axiosInstance}
|
||||
/>
|
||||
<CRow>
|
||||
{endpointsInfo.map((info) => (
|
||||
<CCol sm="12" lg="6" xxl="4" key={createUuid()}>
|
||||
<ApiStatusCard t={t} info={info} reload={reload} />
|
||||
</CCol>
|
||||
))}
|
||||
</CRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemPage;
|
||||
|
||||
@@ -141,3 +141,25 @@ export const testRegex = (value, regexString) => {
|
||||
};
|
||||
|
||||
export const datesSameDay = (first, second) => first.getDate() === second.getDate();
|
||||
|
||||
const units = {
|
||||
year: 24 * 60 * 60 * 1000 * 365,
|
||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
hour: 60 * 60 * 1000,
|
||||
minute: 60 * 1000,
|
||||
second: 1000,
|
||||
};
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { localeMatcher: 'best fit', style: 'long' });
|
||||
export const formatDaysAgo = (d1, d2 = new Date()) => {
|
||||
const convertedTimestamp = unixToDateString(d1);
|
||||
const date = new Date(convertedTimestamp);
|
||||
const elapsed = date - d2;
|
||||
|
||||
for (const [key] of Object.entries(units))
|
||||
if (Math.abs(elapsed) > units[key] || key === 'second')
|
||||
return rtf.format(Math.round(elapsed / units[key]), key);
|
||||
|
||||
return prettyDate(date);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user