[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:
Charles
2022-09-02 18:12:45 +01:00
parent 0c7cd1f299
commit 837a430228
7 changed files with 386 additions and 13 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View 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;

View 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;

View 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);

View File

@@ -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;

View File

@@ -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);
};