1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/web/ synced 2025-03-09 00:00:01 +01:00

Added redux slice for infrastructure page. Changed its components into functional components. Moved some common components and functions into /common and /utils accordingly

Signed-off-by: Andrii Podriez <andrey5577990@gmail.com>
This commit is contained in:
Andrii Podriez 2024-05-21 18:18:37 +02:00 committed by al3xa23
parent 45bac93f1d
commit d46155a0db
23 changed files with 1619 additions and 16 deletions

View file

@ -29,21 +29,20 @@ import Home from './common/home';
import Header from './common/header';
import Menu from './common/menu';
import InfrastructureComponents from './ic/ics';
import InfrastructureComponent from './ic/ic';
import InfrastructureComponent from './pages/infrastructure/ic';
import Dashboard from './dashboard/dashboard';
import Scenarios from './scenario/scenarios';
import Scenario from './scenario/scenario';
import Users from './user/users';
import User from './user/user';
import APIBrowser from './common/api-browser';
import LoginStore from './user/login-store'
import './styles/app.css';
import './styles/login.css';
import branding from './branding/branding';
import Infrastructure from './pages/infrastructure/infrastructure'
class App extends React.Component {
@ -53,10 +52,6 @@ class App extends React.Component {
this.state = {}
}
static getStores() {
return [LoginStore]
}
componentDidMount() {
NotificationsDataManager.setSystem(this.refs.notificationSystem);
@ -126,8 +121,12 @@ class App extends React.Component {
</>
: '' }
{ currentUser.role === "Admin" || pages.infrastructure ? <>
<Route exact path="/infrastructure" component={InfrastructureComponents} />
<Route path="/infrastructure/:ic" component={InfrastructureComponent} />
<Route exact path="/infrastructure">
<Infrastructure />
</Route>
<Route path="/infrastructure/:ic">
<InfrastructureComponent />
</Route>
</>
: '' }
{ pages.account ? <Route path="/account" component={User} /> : '' }

View file

@ -0,0 +1,48 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Button, Modal} from 'react-bootstrap';
class ConfirmCommand extends React.Component {
onModalKeyPress = (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.props.onClose(false);
}
}
render() {
return <Modal keyboard show={this.props.show} onHide={() => this.props.onClose(false)} onKeyPress={this.onModalKeyPress}>
<Modal.Header>
<Modal.Title>Confirm {this.props.command}</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to {this.props.command} <strong>'{this.props.name}'</strong>?
</Modal.Body>
<Modal.Footer>
<Button onClick={() => this.props.onClose(true)}>Cancel</Button>
<Button onClick={() => this.props.onClose(false)}>Confirm</Button>
</Modal.Footer>
</Modal>;
}
}
export default ConfirmCommand;

View file

@ -0,0 +1,23 @@
import { isJSON } from "../utils/isJson";
import ReactJson from "react-json-view";
const RawDataTable = (props) => {
if(props.rawData !== null && isJSON(props.rawData)){
return (
<ReactJson
src={props.rawData}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={1}
/>
)
} else {
return (
<div>No valid JSON raw data available.</div>
)
}
}
export default RawDataTable;

20
src/localStorage.js Normal file
View file

@ -0,0 +1,20 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
//authentication
export const sessionToken = localStorage.getItem("token");
export const currentUser = JSON.parse(localStorage.getItem("currentUser"));

View file

@ -0,0 +1,225 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useSelector, useDispatch } from "react-redux";
import {Table, ButtonColumn, CheckboxColumn, DataColumn, LabelColumn, LinkColumn } from '../../common/table';
import { Badge } from 'react-bootstrap';
import FileSaver from 'file-saver';
import _ from 'lodash';
import moment from 'moment'
import IconToggleButton from "../../common/buttons/icon-toggle-button";
import { checkICsByCategory } from "../../store/icSlice";
import { useState } from "react";
import { stateLabelStyle } from "./styles";
//a Table of IC components of specific category from props.category
//titled with props.title
const ICCategoryTable = (props) => {
const ics = useSelector(state => state.infrastructure.ICsArray);
let currentUser = JSON.parse(localStorage.getItem("currentUser"));
const checkedICs = useSelector(state => state.infrastructure.checkedICsArray);
const [isGenericDisplayed, setIsGenericDisplayed] = useState(false)
const exportIC = (index) => {
// filter properties
let ic = Object.assign({}, ics[index]);
delete ic.id;
// show save dialog
const blob = new Blob([JSON.stringify(ic, null, 2)], { type: 'application/json' });
FileSaver.saveAs(blob, 'ic-' + (_.get(ic, 'name') || 'undefined') + '.json');
}
const modifyUptimeColumn = (uptime, component) => {
if (uptime >= 0) {
let momentDurationFormatSetup = require("moment-duration-format");
momentDurationFormatSetup(moment)
let timeString = moment.duration(uptime, "seconds").humanize();
return <span>{timeString}</span>
}
else {
return <Badge bg="secondary">unknown</Badge>
}
}
const stateUpdateModifier = (updatedAt, component) => {
let dateFormat = 'ddd, DD MMM YYYY HH:mm:ss ZZ';
let dateTime = moment(updatedAt, dateFormat);
return dateTime.fromNow()
}
const statePrio = (state) => {
switch (state) {
case 'running':
case 'starting':
return 1;
case 'paused':
case 'pausing':
case 'resuming':
return 2;
case 'idle':
return 3;
case 'shutdown':
return 4;
case 'error':
return 10;
default:
return 99;
}
}
//if category of the table is manager we need to filter the generic-typed ics
//according to the state of IconToggleButton
let tableData = [];
if(props.category == "manager"){
tableData = ics.filter(ic=> (ic.category == props.category) && (isGenericDisplayed ** (ic.type == "generic")))
}else{
tableData = ics.filter(ic=> ic.category == props.category)
}
tableData.sort((a, b) => {
if (a.state !== b.state) {
return statePrio(a.state) > statePrio(b.state);
}
else if (a.name !== b.name) {
return a.name < b.name;
}
else {
return a.stateUpdatedAt < b.stateUpdatedAt;
}
})
const isLocalIC = (index, ics) => {
let ic = ics[index]
return !ic.managedexternally
}
const checkAllICs = (ics, title) => {
//TODO
}
const isICchecked = (ic) => {
//TODO
return false
}
const areAllChecked = (title) => {
//TODO
return false
}
const onICChecked = (ic, event) => {
//TODO
}
return (<div>
<h2>
{props.title}
{ props.category == "manager" ?
<span className='icon-button'>
<IconToggleButton
childKey={0}
index={1}
onChange={() => setIsGenericDisplayed(!isGenericDisplayed)}
checked={isGenericDisplayed}
checkedIcon='eye'
uncheckedIcon='eye-slash'
tooltipChecked='click to hide generic managers'
tooltipUnchecked='click to show generic managers'
/>
</span>:<></>
}
</h2>
<Table data={tableData}>
<CheckboxColumn
enableCheckAll
onCheckAll={() => checkAllICs(ics, props.title)}
allChecked={areAllChecked(props.title)}
checkboxDisabled={(index) => isLocalIC(index, ics) === true}
checked={(ic) => isICchecked(ic)}
onChecked={(ic, event) => onICChecked(ic, event)}
width='30'
/>
{currentUser.role === "Admin" ?
<DataColumn
title='ID'
dataKey='id'
/>
: <></>
}
<LinkColumn
title='Name'
dataKeys={['name']}
link='/infrastructure/'
linkKey='id'
/>
<LabelColumn
title='State'
labelKey='state'
labelStyle={(state, component) => stateLabelStyle(state, component)}
/>
<DataColumn
title='Type'
dataKeys={['type']}
/>
<DataColumn
title='Uptime'
dataKey='uptime'
modifier={(uptime, component) => modifyUptimeColumn(uptime, component)}
/>
<DataColumn
title='Last Update'
dataKey='stateUpdateAt'
modifier={(uptime, component) => stateUpdateModifier(uptime, component)}
/>
{currentUser.role === "Admin" ?
<ButtonColumn
width='150'
align='right'
editButton
showEditButton ={(index) => isLocalIC(index, ics)}
exportButton
deleteButton
showDeleteButton = {null}
onEdit={index => {}}
onExport={index => exportIC(index)}
onDelete={index => {}}
/>
:
<ButtonColumn
width='50'
exportButton
onExport={(index) => exportIC(index)}
/>
}
</Table>
</div>
);
}
export default ICCategoryTable;

View file

@ -0,0 +1,75 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useEffect, useState } from 'react';
import {Col, Row} from "react-bootstrap";
import IconButton from '../../../common/buttons/icon-button';
import ManagedICsTable from "./managed-ics-table";
import { useDispatch } from 'react-redux';
import { loadICbyId } from '../../../store/icSlice';
import { sessionToken, currentUser } from '../../../localStorage';
import { loadConfig } from '../../../store/configSlice';
import { useSelector } from 'react-redux';
import {refresh, rawDataTable} from "../ic"
import ICParamsTable from '../ic-params-table';
import RawDataTable from '../../../common/rawDataTable';
import { iconStyle, buttonStyle } from "../styles";
const DefaultICPage = (props) => {
const ic = props.ic;
const dispatch = useDispatch();
const refresh = () => {
dispatch(loadICbyId({token: sessionToken, id: ic.id}));
}
useEffect(() => {
}, []);
return (<div className='section'>
<h1>{ic.name}
<span className='icon-button'>
<IconButton
childKey={1}
tooltip='Refresh'
onClick={() => refresh(ic, sessionToken)}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
</Row>
</div>
)
}
export default DefaultICPage;

View file

@ -0,0 +1,88 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React, { useEffect, useState } from 'react';
import {Col, Row} from "react-bootstrap";
import IconButton from '../../../common/buttons/icon-button';
import ManagedICsTable from "./managed-ics-table";
import { useDispatch } from 'react-redux';
import { loadICbyId } from '../../../store/icSlice';
import { sessionToken, currentUser } from '../../../localStorage';
import { loadConfig } from '../../../store/configSlice';
import { useSelector } from 'react-redux';
import {refresh, rawDataTable} from "../ic"
import ICParamsTable from '../ic-params-table';
import RawDataTable from '../../../common/rawDataTable';
import { iconStyle, buttonStyle } from "../styles";
const DefaultManagerPage = (props) => {
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const dispatch = useDispatch();
const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
const [jobName, setJobName] = useState(null);
const refresh = () => {
dispatch(loadICbyId({token: sessionToken, id: ic.id}));
}
useEffect(() => {
}, []);
return (<div className='section'>
<h1>{props.ic.name}
<span className='icon-button'>
<IconButton
childKey={2}
tooltip='Refresh'
onClick={() => refresh()}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
<ManagedICsTable
managedICs={managedICs}
currentUser={currentUser}
/>
</Col>
</Row>
<hr />
<Row>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
</Row>
</div>)
}
export default DefaultManagerPage;

View file

@ -0,0 +1,120 @@
import { useState } from "react";
import { useDispatch } from "react-redux";
import ICParamsTable from "../ic-params-table";
import RawDataTable from '../../../common/rawDataTable';
import { restartIC, shutdownIC, loadICbyId } from "../../../store/icSlice";
import { sessionToken, currentUser } from "../../../localStorage";
import { buttonStyle, iconStyle } from "../styles";
import IconButton from "../../../common/buttons/icon-button";
import {Button, Col, Container, Row} from "react-bootstrap";
import ConfirmCommand from "../../../common/confirm-command";
const GatewayVillasNode = (props) => {
const dispatch = useDispatch();
const ic = props.ic;
const [command, setCommand] = useState("");
const [isCommandConfirmed, setIsCommandConfirmed] = useState(false);
const sendControlCommand = () => {
switch(command){
case "restart":
dispatch(restartIC({apiurl: ic.apiurl}))
case "shutdown":
dispatch(shutdownIC({apiurl: ic.apiurl}))
default:
console.log("Command not found");
}
}
const confirmCommand = (canceled) => {
if(!canceled){
sendControlCommand();
}
setIsCommandConfirmed(false);
setCommand("");
}
return (
<div className='section'>
<h1>{ic.name}
<span className='icon-button'>
<IconButton
childKey={2}
tooltip='Refresh'
onClick={() => dispatch(loadICbyId({id: ic.id, token: sessionToken}))}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
{currentUser.role === "Admin" ?
<div>
<h4>Controls</h4>
<div className='solid-button'>
<Button
variant='secondary'
style={{ margin: '5px' }}
size='lg'
onClick={() => {
setIsCommandConfirmed(true);
setIsCommandConfirmed('restart');
}}>
Restart
</Button>
<Button
variant='secondary'
style={{ margin: '5px' }}
size='lg'
onClick={() => {
setIsCommandConfirmed(true);
setIsCommandConfirmed('shutdown');
}}>
Shutdown
</Button>
</div>
</div>
: <div />
}
<ConfirmCommand
show={isCommandConfirmed}
command={command}
name={ic.name}
onClose={c => confirmCommand(c)}
/>
</Col>
</Row>
<hr/>
<Row>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
<Col>
<h4>Raw Config</h4>
<RawDataTable rawData={ic.statusupdateraw != null ? ic.statusupdateraw.config : null}/>
</Col>
<Col>
<h4>Raw Statistics</h4>
<RawDataTable rawData={ic.statusupdateraw != null ? ic.statusupdateraw.statistics: null}/>
</Col>
</Row>
</div>
)
}
export default GatewayVillasNode;

View file

@ -0,0 +1,170 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useState, useEffect } from "react";
import { Col, Row, Container, Table } from "react-bootstrap";
import IconButton from "../../../common/buttons/icon-button";
import ManagedICsTable from "./managed-ics-table";
import FileSaver from 'file-saver';
import RawDataTable from "../../../common/rawDataTable";
import { downloadGraph } from "../../../utils/icUtils";
import { sessionToken, currentUser } from "../../../localStorage";
import { useDispatch, useSelector } from "react-redux";
import { loadAllICs, loadICbyId } from "../../../store/icSlice";
import ICParamsTable from "../ic-params-table";
import { iconStyle, buttonStyle } from "../styles";
const KubernetesICPage = (props) => {
const dispatch = useDispatch();
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const config = useSelector((state) => state.config.config);
//const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
let namespace = null;
let jobName = null;
let podNames = [];
let clusterName = null;
let rancherURL = null;
let managerIC = ics.find(manager => manager.uuid === ic.manager);
// Take k8s cluster details from managers status update
if (managerIC != null) {
let managerProps = managerIC.statusupdateraw.properties;
if (managerProps != null) {
rancherURL = managerProps.rancher_url;
clusterName = managerProps.cluster_name;
}
}
// Fallback to backend config
if (config != null && config.kubernetes != null) {
let k8s = config.kubernetes;
if (k8s != null) {
if (rancherURL == null) {
rancherURL = k8s.rancher_url
}
if (clusterName == null) {
clusterName = k8s.cluster_name
}
}
}
if (rancherURL != null && clusterName != null) {
if (ic.statusupdateraw != null) {
let icProps = ic.statusupdateraw.properties
if (icProps != null && icProps.namespace != null) {
namespace = icProps.namespace;
jobName = icProps.job_name;
if (icProps.pod_names != null) {
podNames = icProps.pod_names;
}
}
}
}
let rancherTableRows = []
if (rancherURL != null && clusterName != null) {
let baseURL = rancherURL + "/dashboard/c/" + clusterName + "/explorer";
if (namespace != null) {
if (jobName != null) {
let url = baseURL + "/batch.job/" + namespace + "/" + jobName;
rancherTableRows.push(<tr>
<td>Job</td>
<td>{namespace}</td>
<td><a href={url}>{jobName}</a></td>
</tr>)
}
for (const podName of podNames) {
let url = baseURL + "/pod/" + namespace + "/" + podName;
rancherTableRows.push(<tr>
<td>Pod</td>
<td>{namespace}</td>
<td><a href={url}>{podName}</a></td>
</tr>)
}
}
}
const refresh = () => {
dispatch(loadICbyId({token: sessionToken, id: ic.id}));
}
return (
<div className='section'>
<h1>{ic.name}
<span className='icon-button'>
<IconButton
childKey={2}
tooltip='Refresh'
onClick={() => refresh()}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<div>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
<h4>Resources</h4>
<Table striped size="sm">
<thead>
<tr>
<th>Kind</th>
<th>Namespace</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{rancherTableRows}
</tbody>
</Table>
</Col>
</Row>
<hr/>
<Row>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
</Row>
</div>
</div>
)
}
export default KubernetesICPage;

View file

@ -0,0 +1,59 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import React from 'react';
import { Table, LabelColumn, LinkColumn, DataColumn } from '../../../common/table';
import { stateLabelStyle } from "../styles";
class ManagedICsTable extends React.Component {
render() {
return (<div>
<h3>Managed ICs:</h3>
<Table data={this.props.managedICs}>
{this.props.currentUser.role === "Admin" ?
<DataColumn
title='ID'
dataKey='id'
/>
: <></>
}
<LinkColumn
title='Name'
dataKeys={['name']}
link='/infrastructure/'
linkKey='id'
/>
<LabelColumn
title='State'
labelKey='state'
labelStyle={(state, component) => stateLabelStyle(state, component)}
/>
<DataColumn
title='Type'
dataKeys={['type']}
/>
</Table>
</div>
)
}
}
export default ManagedICsTable;

View file

@ -0,0 +1,99 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useState, useEffect } from "react";
import { Col, Row } from "react-bootstrap";
import IconButton from "../../../common/buttons/icon-button";
import ManagedICsTable from "./managed-ics-table";
import FileSaver from 'file-saver';
import RawDataTable from "../../../common/rawDataTable";
import { downloadGraph } from "../../../utils/icUtils";
import { sessionToken, currentUser } from "../../../localStorage";
import { useDispatch, useSelector } from "react-redux";
import { loadAllICs, loadICbyId } from "../../../store/icSlice";
import ICParamsTable from "../ic-params-table";
import { iconStyle, buttonStyle } from "../styles";
const ManagerVillasNode = (props) => {
const dispatch = useDispatch();
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
const graphURL = ic.apiurl !== "" ? ic.apiurl + "/graph.svg" : "";
const refresh = () => {
dispatch(loadICbyId({token: sessionToken, id: ic.id}));
}
return (
<div className='section'>
<h1>{ic.name}
<span className='icon-button'>
<IconButton
childKey={2}
tooltip='Refresh'
onClick={() => refresh()}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
<ManagedICsTable
managedICs={managedICs}
currentUser={currentUser}
/>
</Col>
</Row>
<hr />
<Row>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
<Col>
<div className='section-buttons-group-right'>
<IconButton
childKey={0}
tooltip='Download Graph'
onClick={() => downloadGraph(graphURL)}
icon='download'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</div>
<h4>Graph</h4>
<div>
<img alt={"Graph image download failed and/or incorrect image API URL"} src={graphURL} />
</div>
</Col>
</Row>
</div>)
}
export default ManagerVillasNode;

View file

@ -0,0 +1,81 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useState, useEffect } from "react";
import { Col, Row, Container } from "react-bootstrap";
import IconButton from "../../../common/buttons/icon-button";
import ManagedICsTable from "./managed-ics-table";
import RawDataTable from "../../../common/rawDataTable";
import { sessionToken, currentUser } from "../../../localStorage";
import { useDispatch, useSelector } from "react-redux";
import { loadAllICs, loadICbyId } from "../../../store/icSlice";
import ICParamsTable from "../ic-params-table";
import { iconStyle, buttonStyle } from "../styles";
const ManagerVillasRelay = (props) => {
const dispatch = useDispatch();
const ic = props.ic;
const ics = useSelector((state) => state.infrastructure.ICsArray);
const managedICs = ics.filter(managedIC => managedIC.category !== "manager" && managedIC.manager === ic.uuid);
const refresh = () => {
dispatch(loadICbyId({token: sessionToken, id: ic.id}));
}
return (<div className='section'>
<h1>{ic.name}
<span className='icon-button'>
<IconButton
childKey={2}
tooltip='Refresh'
onClick={() => refresh()}
icon='sync-alt'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
</h1>
<Container>
<Row>
<Col>
<h4>Properties</h4>
<ICParamsTable ic={ic}/>
</Col>
<Col>
<ManagedICsTable
managedICs={managedICs}
currentUser={currentUser}
/>
</Col>
</Row>
<Row>
<Col>
<h4>Raw Status</h4>
<RawDataTable rawData={ic.statusupdateraw}/>
</Col>
</Row>
</Container>
</div>)
}
export default ManagerVillasRelay;

View file

@ -0,0 +1,74 @@
import { Table, } from 'react-bootstrap';
import moment from 'moment';
import { isJSON } from '../../utils/isJson';
import ReactJson from 'react-json-view';
const ICParamsTable = (props) => {
const ic = props.ic;
return(
<Table striped size="sm">
<thead>
<tr><th>Property</th><th>Value</th></tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>{ic.name}</td>
</tr>
<tr>
<td>Description</td>
<td>{ic.description}</td>
</tr>
<tr>
<td>UUID</td
><td>{ic.uuid}</td>
</tr>
<tr>
<td>State</td>
<td>{ic.state}</td>
</tr>
<tr>
<td>Category</td>
<td>{ic.category}</td>
</tr>
<tr>
<td>Type</td>
<td>{ic.type}</td>
</tr>
<tr>
<td>Uptime</td>
<td>{moment.duration(ic.uptime, "seconds").humanize()}</td>
</tr>
<tr>
<td>Location</td>
<td>{ic.location}</td>
</tr>
<tr>
<td>Websocket URL</td>
<td>{ic.websocketurl}</td>
</tr>
<tr>
<td>API URL</td>
<td>{ic.apiurl}</td>
</tr>
<tr>
<td>Start parameter schema</td>
<td>
{isJSON(ic.startparameterschema) ?
<ReactJson
src={ic.startparameterschema}
name={false}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
collapsed={0}
/> : <div>No Start parameter schema JSON available.</div>}
</td>
</tr>
</tbody>
</Table>
)
}
export default ICParamsTable;

View file

@ -0,0 +1,63 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useEffect, useRef } from "react";
import { sessionToken, currentUser } from "../../localStorage";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from 'react-router-dom';
import { loadAllICs, loadICbyId } from "../../store/icSlice";
import { loadConfig } from "../../store/configSlice";
import DefaultManagerPage from "./ic-pages/default-manager-page";
import GatewayVillasNode from "./ic-pages/gateway-villas-node";
import ManagerVillasNode from "./ic-pages/manager-villas-node";
import ManagerVillasRelay from "./ic-pages/manager-villas-relay";
import KubernetesICPage from "./ic-pages/kubernetes-ic-page";
import DefaultICPage from "./ic-pages/default-ic-page";
const InfrastructureComponent = (props) => {
const params = useParams();
const id = params.ic;
const dispatch = useDispatch();
const ic = useSelector(state => state.infrastructure.currentIC);
const isICLoading = useSelector(state => state.infrastructure.isCurrentICLoading);
useEffect(() => {
dispatch(loadAllICs({token: sessionToken}));
dispatch(loadICbyId({token: sessionToken, id: id}));
dispatch(loadConfig({token: sessionToken}));
}, []);
if(ic == null || ic === undefined){
return <div>Loading...</div>
} else if(ic.category ==="gateway" && ic.type === "villas-node"){
return <GatewayVillasNode ic={ic}/>
} else if(ic.category ==="manager" && ic.type === "villas-node"){
return <ManagerVillasNode ic={ic}/>
} else if(ic.category ==="manager" && ic.type === "villas-relay"){
return <ManagerVillasRelay ic={ic}/>
} else if(ic.category ==="manager") {
return <DefaultManagerPage ic={ic} />
} else if(ic.category === "simulator" && ic.type === "kubernetes"){
return <KubernetesICPage ic={ic}/>
} else {
return <DefaultICPage ic={ic} />
}
}
export default InfrastructureComponent;

View file

@ -0,0 +1,140 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { Badge } from 'react-bootstrap';
import { loadAllICs, loadICbyId } from "../../store/icSlice";
import { set } from "lodash";
import IconButton from "../../common/buttons/icon-button";
import ICCategoryTable from "./ic-category-table";
import { sessionToken, currentUser } from "../../localStorage";
const Infrastructure = (props) => {
const dispatch = useDispatch();
const ICsArray = useSelector(state => state.infrastructure.ICsArray);
//track status of the modals
const [isEditModalOpened, setIsEditModalOpened] = useState(false);
const [isNewModalOpened, setIsNewModalOpened] = useState(false);
const [isImportModalOpened, setIsImportModalOpened] = useState(false);
const [isDeleteModalOpened, setIsDeleteModalOpened] = useState(false);
const [isICModalOpened, setIsICModalOpened] = useState(false);
const [checkedICs, setCheckedICs] = useState([]);
const currentUser = JSON.parse(localStorage.getItem("currentUser"));
useEffect(() => {
//load array of ics and start a timer for periodic refresh
dispatch(loadAllICs({token: sessionToken}));
let timer = window.setInterval(() => refresh(), 10000);
return () => {
window.clearInterval(timer);
}
}, []);
const refresh = () => {
//if none of the modals are currently opened, we reload ics array
if(!(isEditModalOpened || isDeleteModalOpened || isICModalOpened)){
dispatch(loadAllICs({token: sessionToken}));
}
}
const buttonStyle = {
marginLeft: '10px',
}
const iconStyle = {
height: '30px',
width: '30px'
}
return (
<div>
<div className='section'>
<h1>Infrastructure
{//TODO
/* {currentUser.role === "Admin" ?
<span className='icon-button'>
<IconButton
childKey={1}
tooltip='Add Infrastructure Component'
onClick={() => setIsNewModalOpened(true)}
icon='plus'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
<IconButton
childKey={1}
tooltip='Import Infrastructure Component'
onClick={() => setIsImportModalOpened(true)}
icon='upload'
buttonStyle={buttonStyle}
iconStyle={iconStyle}
/>
</span>
:
<span />
} */}
</h1>
<ICCategoryTable
title={"IC Managers"}
category={"manager"}
/>
<ICCategoryTable
title={"Simulators"}
category={"simulator"}
/>
<ICCategoryTable
title={"Gateways"}
category={"gateway"}
/>
<ICCategoryTable
title={"Services"}
category={"service"}
/>
<ICCategoryTable
title={"Equipment"}
category={"equipment"}
/>
</div>
{//TODO
/* <NewICDialog show={this.state.newModal} onClose={data => this.closeNewModal(data)} managers={this.state.managers} />
<EditICDialog show={this.state.editModal} onClose={data => this.closeEditModal(data)} ic={this.state.modalIC} />
<ImportICDialog show={this.state.importModal} onClose={data => this.closeImportModal(data)} />
<DeleteDialog title="infrastructure-component" name={this.state.modalIC.name || 'Unknown'} show={this.state.deleteModal} onClose={(e) => this.closeDeleteModal(e)} /> */}
</div>
);
}
export default Infrastructure;

View file

@ -0,0 +1,58 @@
import { isICOutdated } from "../../utils/icUtils";
export const buttonStyle = {
marginLeft: '5px',
}
export const iconStyle = {
height: '25px',
width: '25px'
}
//outputs a corresponding style for a LabelColumn Component
export const stateLabelStyle = (state, component) => {
let style = [];
switch (state) {
case 'error':
style[0] = 'danger';
break;
case 'idle':
style[0] = 'primary';
break;
case 'starting':
style[0] = 'info';
break;
case 'running':
style[0] = 'success';
break;
case 'pausing':
style[0] = 'info';
break;
case 'paused':
style[0] = 'info';
break;
case 'resuming':
style[0] = 'warning';
break;
case 'stopping':
style[0] = 'warning';
break;
case 'resetting':
style[0] = 'warning';
break;
case 'shuttingdown':
style[0] = 'warning';
break;
case 'shutdown':
style[0] = 'warning';
break;
default:
style[0] = 'secondary';
}
style[1] = isICOutdated(component) && state !== 'shutdown' ? 'badge-outdated' : ''
return style;
}

54
src/store/configSlice.js Normal file
View file

@ -0,0 +1,54 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import RestAPI from '../common/api/rest-api';
const configSlice = createSlice({
name: 'config',
initialState: {
config: {},
isLoading: false
},
extraReducers: builder => {
builder
.addCase(loadConfig.pending, (state, action) => {
state.isLoading = true
})
.addCase(loadConfig.fulfilled, (state, action) => {
state.isLoading = false
state.config = action.payload;
console.log("fetched config", action.payload)
})
}
});
//loads all ICs and saves them in the store
export const loadConfig = createAsyncThunk(
'config/loadConfig',
async (data) => {
try {
const res = await RestAPI.get("/api/v2/config", null);
console.log("response:", res)
return res;
} catch (error) {
console.log("Error loading config", error);
}
}
);
export default configSlice.reducer;

131
src/store/icSlice.js Normal file
View file

@ -0,0 +1,131 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import {createSlice, createAsyncThunk} from '@reduxjs/toolkit'
import RestAPI from '../common/api/rest-api';
import { sessionToken } from '../localStorage';
const icSlice = createSlice({
name: 'infrastructure',
initialState: {
ICsArray: [],
checkedICsArray: [],
isLoading: false,
currentIC: {},
isCurrentICLoading: false
},
reducers: {
checkICsByCategory: (state, args) => {
const category = args.payload;
for(const ic in state.ICsArray){
if (ic.category == category) state.checkedICsArray.push(ic)
}
}
},
extraReducers: builder => {
builder
.addCase(loadAllICs.pending, (state, action) => {
state.isLoading = true
})
.addCase(loadAllICs.fulfilled, (state, action) => {
state.ICsArray = action.payload;
console.log("fetched ICs")
})
.addCase(loadICbyId.pending, (state, action) => {
state.isCurrentICLoading = true
})
.addCase(loadICbyId.fulfilled, (state, action) => {
state.isCurrentICLoading = false
state.currentIC = action.payload;
console.log("fetched IC", state.currentIC.name)
})
//TODO
// .addCase(restartIC.fullfilled, (state, action) => {
// console.log("restart fullfilled")
// //loadAllICs({token: sessionToken})
// })
// .addCase(shutdownIC.fullfilled, (state, action) => {
// console.log("shutdown fullfilled")
// //loadAllICs({token: sessionToken})
// })
}
});
//loads all ICs and saves them in the store
export const loadAllICs = createAsyncThunk(
'infrastructure/loadAllICs',
async (data) => {
try {
const res = await RestAPI.get('/api/v2/ic', data.token);
return res.ics;
} catch (error) {
console.log("Error loading ICs data: ", error);
}
}
);
//loads one IC by its id
export const loadICbyId = createAsyncThunk(
'infrastructure/loadICbyId',
async (data) => {
try {
const res = await RestAPI.get('/api/v2/ic/' + data.id, data.token);
return res.ic;
} catch (error) {
console.log("Error loading IC (id=" + data.id + ") : ", error);
}
}
)
//TODO
//restarts ICs
export const restartIC = createAsyncThunk(
'infrastructure/restartIC',
async (data) => {
try {
const url = data.apiurl + '/restart'
const res = await RestAPI.post(data.apiurl, null);
console.log(res)
return res;
} catch (error) {
console.log("Error restarting IC: ", error);
}
}
)
//restarts ICs
export const shutdownIC = createAsyncThunk(
'infrastructure/shutdownIC',
async (data) => {
try {
const url = data.apiurl + '/shutdown'
const res = await RestAPI.post(data.apiurl, null);
console.log(res)
return res;
} catch (error) {
console.log("Error shutting IC down: ", error);
}
}
)
export const {checkICsByCategory} = icSlice.actions;
export default icSlice.reducer;

View file

@ -15,12 +15,17 @@
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { configureStore } from "@reduxjs/toolkit"
import userReducer from './userSlice'
import { configureStore } from "@reduxjs/toolkit";
import userReducer from './userSlice';
import icReducer from './icSlice';
import configReducer from './configSlice'
export const store = configureStore({
reducer: {
user: userReducer
user: userReducer,
infrastructure: icReducer,
config: configReducer
},
devTools: true
})

View file

@ -21,8 +21,12 @@ import RestAPI from '../common/api/rest-api';
import ICDataDataManager from '../ic/ic-data-data-manager';
const userSlice = createSlice({
name: 'login',
initialState: {currentUser: null, currentToken: null, isLoading: false, loginMessage: ''},
name: 'user',
initialState: {
currentUser: null,
currentToken: null,
isLoading: false,
loginMessage: ''},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state, action) => {
@ -62,7 +66,6 @@ export const login = createAsyncThunk(
async (userData, thunkAPI) => {
try {
const res = await RestAPI.post('/api/v2/authenticate/internal', userData)
return {user: res.user, token: res.token}
} catch(error) {
console.log('Error while trying to log in: ', error)

42
src/styles/login.css Normal file
View file

@ -0,0 +1,42 @@
/**
* This file is part of VILLASweb.
*
* VILLASweb is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VILLASweb is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
.login-parent {
display: flex;
max-width: 800px;
margin: 30px auto;
}
.login-welcome {
float: right;
max-width: 400px;
padding: 15px 20px;
border-radius: var(--borderradius) 0px 0px var(--borderradius);
background-color: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 9px 18px 0 rgba(0, 0, 0, 0.1);
}
.login-container {
float: left;
max-width: 400px;
border-radius: 0px var(--borderradius) var(--borderradius) 0px;
padding: 15px 20px;
background-color: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 9px 18px 0 rgba(0, 0, 0, 0.1);
}

13
src/utils/icUtils.js Normal file
View file

@ -0,0 +1,13 @@
export const isICOutdated = (component) => {
if (!component.stateUpdateAt)
return true;
const fiveMinutes = 5 * 60 * 1000;
return Date.now() - new Date(component.stateUpdateAt) > fiveMinutes;
}
export const downloadGraph = async (url) => {
let blob = await fetch(url).then(r => r.blob())
FileSaver.saveAs(blob, this.props.ic.name + ".svg");
}

13
src/utils/isJson.js Normal file
View file

@ -0,0 +1,13 @@
export const isJSON = (data) => {
if (data === undefined || data === null) {
return false;
}
let str = JSON.stringify(data);
try {
JSON.parse(str)
}
catch (ex) {
return false
}
return true
}