Merged PR 60: System card

- System card
- SessionsCount
- SystemVersionComponent
master
Tudor Stanciu 2022-09-25 19:07:15 +00:00
commit 9443907cba
28 changed files with 543 additions and 61 deletions

View File

@ -21,8 +21,10 @@ RUN npm install -g serve
# environment variables
ENV Author="Tudor Stanciu"
ARG APP_VERSION=0.0.0.0
ARG APP_VERSION=0.0.0
ARG APP_DATE=1900-01-01
ENV APP_VERSION=${APP_VERSION}
ENV APP_DATE=${REACT_APP_DATE}
#set workdir to root
WORKDIR /

View File

@ -1,5 +1,7 @@
const dev = {
NODE_ENV: "development",
APP_VERSION: "0.0.0",
APP_DATE: "1900-01-01",
REVERSE_PROXY_API_URL: "http://localhost:5050",
CHATBOT_API_URL: "http://localhost:5061",
REVERSE_PROXY_DOCS_URL: "https://toodle.ddns.net/hedgedoc/s/UkJ6S5NJz"
@ -7,6 +9,8 @@ const dev = {
const prod = {
NODE_ENV: "production",
APP_VERSION: "0.0.0",
APP_DATE: "1900-01-01",
PUBLIC_URL: "/reverse-proxy",
REVERSE_PROXY_API_URL: "https://toodle.ddns.net/reverse-proxy-api",
CHATBOT_API_URL: "https://toodle.ddns.net/chatbot-api",

View File

@ -2,21 +2,14 @@
Material UI v4: https://v4.mui.com/components/lists/
https://v4.mui.com/components/material-icons/
****************************************************************
withTranslation()(LegacyComponentClass)
const { t } = this.props;
TO DO:
******
Diagrama cu toate redirecturile care trec prin server;
https://github.com/projectstorm/react-diagrams
https://antonioru.github.io/beautiful-react-diagrams/#/Diagram%20Component
****************************************************************
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t, i18n } = useTranslation();
import { makeStyles, useTheme } from "@material-ui/core/styles";
const theme = useTheme();
https://www.flaticon.com/free-icon/wizard_2534554?term=wizard&page=1&position=64
https://lucasbassetti.com.br/react-simple-chatbot/#/docs/previous-value

View File

@ -106,13 +106,13 @@
"Subtitle": "Expand to see details",
"Thoughts": "This reverse proxy is the only open gate to a secret creation land. There any impulse or thought can fly free and can be materialized without limits. If you don't believe it, ask the ",
"Wizard": "wizard",
"ServerHostName": "Server host",
"ApiHostName": "API host",
"HostName": "Server host",
"SessionsCount": "Sessions count",
"Domain": "Domain",
"ActiveSession": "Active session",
"ActiveSessionSubtitle": "Expand to see forwards",
"DDNSProvider": "Dynamic DNS Provider",
"Details": "Details"
"About": "About"
},
"Charts": {
"Server": {
@ -159,6 +159,24 @@
}
},
"System": {
"Title": "System",
"Subtitle": "Expand to see details",
"Server": {
"HostName": "Server host",
"Platform": "Server platform"
},
"Api": {
"HostName": "API host",
"Platform": "API platform"
},
"Description": "This system is composed of three micro services, each with a well-defined role. The server (reverse proxy) is the only one that can work completely independently of the others, the api and the UI being auxiliary and having a role of visualizing the server's activity.",
"Versions": {
"Title": "Component versions",
"Server": "Server: {{version}}",
"Api": "API: {{version}}",
"Frontend": "UI: {{version}}"
},
"LastUpdateDate": "Last update date: {{date}}",
"Components": {
"Server": "Server:",
"Api": "API:",

View File

@ -97,13 +97,13 @@
"Subtitle": "Extindeţi pentru a vedea detalii",
"Thoughts": "Acest reverse proxy este singura poartă deschisă către un teren secret al creației. Acolo orice impuls sau gând poate zbura liber și poate fi materializat fără limite. Dacă nu crezi, întreabă-l pe ",
"Wizard": "vrăjitor",
"ServerHostName": "Gazdă server",
"ApiHostName": "Gazdă API",
"HostName": "Gazdă server",
"SessionsCount": "Număr sesiuni",
"Domain": "Domeniu",
"ActiveSession": "Sesiune activă",
"ActiveSessionSubtitle": "Extindeţi pentru a vedea redirectările",
"DDNSProvider": "Furnizor DNS dinamic",
"Details": "Detalii"
"About": "Despre"
},
"Charts": {
"Server": {
@ -150,6 +150,24 @@
}
},
"System": {
"Title": "Sistem",
"Subtitle": "Extindeţi pentru a vedea detalii",
"Server": {
"HostName": "Gazdă server",
"Platform": "Platformă server"
},
"Api": {
"HostName": "Gazdă API",
"Platform": "Platformă API"
},
"Description": "Acest sistem este compus din trei microservicii, fiecare avand un rol bine definit. Serverul (reverse proxy-ul) este singurul care poate funcționa complet independent de celelalte, API-ul și UI-ul fiind auxiliare și având un rol de vizualizare a activității serverului.",
"Versions": {
"Title": "Versiuni componente",
"Server": "Server: {{version}}",
"Api": "API: {{version}}",
"Frontend": "UI: {{version}}"
},
"LastUpdateDate": "Data ultimei actualizări: {{date}}",
"Components": {
"Server": "Server:",
"Api": "API:",

View File

@ -1,6 +1,7 @@
import React from "react";
import ServerContainer from "../../features/server/components/ServerContainer";
import ActiveSessionContainer from "../../features/server/components/ActiveSessionContainer";
import SystemContainer from "../../features/system/components/SystemContainer";
const HomePage = () => {
return (
@ -9,6 +10,10 @@ const HomePage = () => {
<br />
<br />
<ActiveSessionContainer />
<br />
<br />
<SystemContainer />
<br />
</>
);
};

View File

@ -4,17 +4,13 @@ import { bindActionCreators } from "redux";
import PropTypes from "prop-types";
import AboutComponent from "./AboutComponent";
import TechnologiesComponent from "./TechnologiesComponent";
import { useDocumentation } from "../../../hooks";
const AboutContainer = () => {
const handleOpenDocumentation = event => {
const url = process.env.REVERSE_PROXY_DOCS_URL;
window.open(url, "_blank");
event.preventDefault();
};
const { openDocumentation } = useDocumentation();
return (
<>
<AboutComponent onOpenDocumentation={handleOpenDocumentation} />
<AboutComponent onOpenDocumentation={openDocumentation} />
<br />
<br />
<TechnologiesComponent />

View File

@ -13,17 +13,6 @@ export function loadServerData() {
};
}
export function loadSystemVersion() {
return async function (dispatch) {
try {
const data = await dispatch(sendHttpRequest(api.getSystemVersion()));
dispatch({ type: types.LOAD_SYSTEM_VERSION_SUCCESS, payload: data });
} catch (error) {
throw error;
}
};
}
export function loadActiveSession() {
return async function(dispatch) {
try {

View File

@ -1,3 +1,2 @@
export const LOAD_SYSTEM_VERSION_SUCCESS = "LOAD_SYSTEM_VERSION_SUCCESS";
export const LOAD_ACTIVE_SESSION_SUCCESS = "LOAD_ACTIVE_SESSION_SUCCESS";
export const LOAD_SERVER_DATA_SUCCESS = "LOAD_SERVER_DATA_SUCCESS";

View File

@ -2,11 +2,9 @@ import { get } from "../../api/axiosApi";
const baseUrl = process.env.REVERSE_PROXY_API_URL;
const getServerData = () => get(`${baseUrl}/server/data`);
const getSystemVersion = () => get(`${baseUrl}/system/version`);
const getActiveSession = () => get(`${baseUrl}/server/active-session`);
export default {
getServerData,
getSystemVersion,
getActiveSession
};

View File

@ -65,7 +65,7 @@ const ServerComponent = ({
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={handleDetailsClick}>{t("Server.Details")}</MenuItem>
<MenuItem onClick={handleDetailsClick}>{t("Server.About")}</MenuItem>
</Menu>
<Card>
<CardHeader

View File

@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import PropTypes from "prop-types";
import { loadServerData, loadSystemVersion } from "../actionCreators";
import { loadServerData } from "../actionCreators";
import ServerComponent from "./ServerComponent";
import { withRouter } from "react-router-dom";
import { summonWizard } from "../../chatbot/actionCreators";
@ -10,7 +10,6 @@ import { summonWizard } from "../../chatbot/actionCreators";
const ServerContainer = ({ actions, data, serverHost, history }) => {
useEffect(() => {
actions.loadServerData();
actions.loadSystemVersion();
}, []);
const openAbout = event => {
@ -50,10 +49,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(
{ loadServerData, loadSystemVersion, summonWizard },
dispatch
)
actions: bindActionCreators({ loadServerData, summonWizard }, dispatch)
};
}

View File

@ -24,13 +24,13 @@ const ServerSummary = ({
</Grid>
<Grid item xs={6} sm={3} md={3}>
{`${t("Server.ServerHostName")}: `}
{`${t("Server.HostName")}: `}
<span className={classes.value}>{serverHost || ""}</span>
</Grid>
<Grid item xs={6} sm={3} md={3}>
{`${t("Server.ApiHostName")}: `}
<span className={classes.value}>{data.hosts.api}</span>
{`${t("Server.SessionsCount")}: `}
<span className={classes.value}>{data.sessionsCount}</span>
</Grid>
<Grid item xs={6} sm={3} md={3}>

View File

@ -9,12 +9,6 @@ export default function serverReducer(state = initialState.server, action) {
data: { ...action.payload, loading: false, loaded: true }
};
case types.LOAD_SYSTEM_VERSION_SUCCESS:
return {
...state,
...action.payload
};
case types.LOAD_ACTIVE_SESSION_SUCCESS:
return {
...state,

View File

@ -0,0 +1,25 @@
import * as types from "./actionTypes";
import api from "./api";
import { sendHttpRequest } from "../../redux/actions/httpActions";
export function loadSystemData() {
return async function(dispatch) {
try {
const data = await dispatch(sendHttpRequest(api.getSystemData()));
dispatch({ type: types.LOAD_SYSTEM_DATA_SUCCESS, payload: data });
} catch (error) {
throw error;
}
};
}
export function loadSystemVersion() {
return async function(dispatch) {
try {
const data = await dispatch(sendHttpRequest(api.getSystemVersion()));
dispatch({ type: types.LOAD_SYSTEM_VERSION_SUCCESS, payload: data });
} catch (error) {
throw error;
}
};
}

View File

@ -0,0 +1,2 @@
export const LOAD_SYSTEM_DATA_SUCCESS = "LOAD_SYSTEM_DATA_SUCCESS";
export const LOAD_SYSTEM_VERSION_SUCCESS = "LOAD_SYSTEM_VERSION_SUCCESS";

View File

@ -0,0 +1,10 @@
import { get } from "../../api/axiosApi";
const baseUrl = process.env.REVERSE_PROXY_API_URL;
const getSystemData = () => get(`${baseUrl}/system/data`);
const getSystemVersion = () => get(`${baseUrl}/system/version`);
export default {
getSystemData,
getSystemVersion
};

View File

@ -0,0 +1,122 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import clsx from "clsx";
import {
Card,
CardHeader,
CardContent,
CardActions,
Collapse,
Avatar,
IconButton,
Tooltip,
Menu,
MenuItem
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import BubbleChartIcon from "@material-ui/icons/BubbleChart";
import styles from "../../../components/common/styles/expandableCardStyles";
import SystemSummary from "./SystemSummary";
import { useTranslation } from "react-i18next";
import SystemExtensionArea from "./SystemExtensionArea";
import LibraryBooksIcon from "@material-ui/icons/LibraryBooks";
import { useDocumentation } from "../../../hooks";
const useStyles = makeStyles(styles);
const SystemComponent = ({ data, onRedirect }) => {
const classes = useStyles();
const { t } = useTranslation();
const { openDocumentation } = useDocumentation();
const [expanded, setExpanded] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleExpandClick = () => {
setExpanded(!expanded);
};
const handleMoreClick = event => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<Menu
id="system-actions-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={onRedirect("/about", handleClose)}>
{t("Menu.About")}
</MenuItem>
<MenuItem onClick={onRedirect("/sessions", handleClose)}>
{t("Menu.Sessions")}
</MenuItem>
<MenuItem onClick={onRedirect("/release-notes", handleClose)}>
{t("Menu.ReleaseNotes")}
</MenuItem>
</Menu>
<Card>
<CardHeader
avatar={
<Avatar aria-label="recipe" className={classes.avatar}>
<BubbleChartIcon />
</Avatar>
}
action={
<IconButton
aria-label="more"
onClick={handleMoreClick}
color="primary"
>
<MoreVertIcon />
</IconButton>
}
title={<strong>{t("System.Title")}</strong>}
subheader={t("System.Subtitle")}
/>
<CardContent>
{data.loaded && <SystemSummary data={data} />}
</CardContent>
<CardActions disableSpacing>
<Tooltip title={t("About.Actions.Documentation")}>
<IconButton aria-label="documentation" onClick={openDocumentation}>
<LibraryBooksIcon />
</IconButton>
</Tooltip>
<IconButton
className={clsx(classes.expand, {
[classes.expandOpen]: expanded
})}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</IconButton>
</CardActions>
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent>
<SystemExtensionArea />
</CardContent>
</Collapse>
</Card>
</>
);
};
SystemComponent.propTypes = {
data: PropTypes.object.isRequired,
onRedirect: PropTypes.func.isRequired
};
export default SystemComponent;

View File

@ -0,0 +1,44 @@
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import PropTypes from "prop-types";
import { loadSystemData } from "../actionCreators";
import SystemComponent from "./SystemComponent";
import { withRouter } from "react-router-dom";
const SystemContainer = ({ actions, data, history }) => {
useEffect(() => {
actions.loadSystemData();
}, []);
const handleRedirect = (route, callback) => event => {
history.push(route);
event.preventDefault();
callback && callback();
};
return <SystemComponent data={data} onRedirect={handleRedirect} />;
};
SystemContainer.propTypes = {
actions: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
data: state.system.data
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ loadSystemData }, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(SystemContainer));

View File

@ -0,0 +1,8 @@
import React from "react";
import SystemVersionContainer from "./version/SystemVersionContainer";
const SystemExtensionArea = () => {
return <SystemVersionContainer />;
};
export default SystemExtensionArea;

View File

@ -0,0 +1,49 @@
import React from "react";
import PropTypes from "prop-types";
import { Grid, Typography } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
import styles from "../../../components/common/styles/gridStyles";
const useStyles = makeStyles(styles);
const SystemSummary = ({ data }) => {
const classes = useStyles();
const { t } = useTranslation();
return (
<Grid container>
<Grid item xs={6} sm={3} md={3}>
{`${t("System.Server.HostName")}: `}
<span className={classes.value}>{data.server.hostName}</span>
</Grid>
<Grid item xs={6} sm={3} md={3}>
{`${t("System.Server.Platform")}: `}
<span className={classes.value}>{data.server.platform}</span>
</Grid>
<Grid item xs={6} sm={3} md={3}>
{`${t("System.Api.HostName")}: `}
<span className={classes.value}>{data.api.hostName}</span>
</Grid>
<Grid item xs={6} sm={3} md={3}>
{`${t("System.Api.Platform")}: `}
<span className={classes.value}>{data.api.platform}</span>
</Grid>
<Grid item xs={12} sm={12} md={12}>
<Typography variant="body2" gutterBottom color="textSecondary">
{t("System.Description")}
</Typography>
</Grid>
</Grid>
);
};
SystemSummary.propTypes = {
data: PropTypes.object.isRequired
};
export default SystemSummary;

View File

@ -0,0 +1,134 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Typography,
List,
ListItem,
ListItemText,
ListItemAvatar
} from "@material-ui/core";
import Avatar from "@material-ui/core/Avatar";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import DnsRoundedIcon from "@material-ui/icons/DnsRounded";
import SettingsInputSvideoIcon from "@material-ui/icons/SettingsInputSvideo";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(theme => {
debugger;
return {
root: {
display: "flex",
flexDirection: "row",
padding: 0
},
value: {
fontSize: "0.9rem",
fontWeight: theme.typography.fontWeightMedium
}
};
});
const SystemVersionComponent = ({ data }) => {
const classes = useStyles();
const { t } = useTranslation();
const lastUpdateDate = useMemo(() => {
const format = "DD-MM-YYYY HH:mm:ss";
const server = t("DATE_FORMAT", {
date: {
value: data.server.lastUpdateDate,
format
}
});
const api = t("DATE_FORMAT", {
date: {
value: data.api.lastUpdateDate,
format
}
});
const frontend = t("DATE_FORMAT", {
date: {
value: process.env.APP_DATE,
format
}
});
return { server, api, frontend };
}, [data, t]);
return (
<>
<Typography variant="subtitle1" color="textSecondary">
{t("System.Versions.Title")}
</Typography>
<List className={classes.root}>
<ListItem>
<ListItemAvatar>
<Avatar>
<DnsRoundedIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("System.Versions.Server", {
version: data.server.version
})}
</span>
}
secondary={t("System.LastUpdateDate", {
date: lastUpdateDate.server
})}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<SettingsInputSvideoIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("System.Versions.Api", {
version: data.api.version
})}
</span>
}
secondary={t("System.LastUpdateDate", {
date: lastUpdateDate.api
})}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<WebAssetIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("System.Versions.Frontend", {
version: process.env.APP_VERSION
})}
</span>
}
secondary={t("System.LastUpdateDate", {
date: lastUpdateDate.frontend
})}
/>
</ListItem>
</List>
</>
);
};
SystemVersionComponent.propTypes = {
data: PropTypes.object.isRequired
};
export default SystemVersionComponent;

View File

@ -0,0 +1,36 @@
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import PropTypes from "prop-types";
import { loadSystemVersion } from "../../actionCreators";
import SystemVersionComponent from "./SystemVersionComponent";
const SystemVersionContainer = ({ actions, data }) => {
useEffect(() => {
actions.loadSystemVersion();
}, []);
return <>{data.loaded && <SystemVersionComponent data={data} />}</>;
};
SystemVersionContainer.propTypes = {
actions: PropTypes.object.isRequired,
data: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
data: state.system.version
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ loadSystemVersion }, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(SystemVersionContainer);

View File

@ -0,0 +1,21 @@
import * as types from "./actionTypes";
import initialState from "../../redux/reducers/initialState";
export default function systemReducer(state = initialState.system, action) {
switch (action.type) {
case types.LOAD_SYSTEM_DATA_SUCCESS:
return {
...state,
data: { ...action.payload, loading: false, loaded: true }
};
case types.LOAD_SYSTEM_VERSION_SUCCESS:
return {
...state,
version: { ...action.payload, loading: false, loaded: true }
};
default:
return state;
}
}

3
src/hooks/index.js Normal file
View File

@ -0,0 +1,3 @@
import useDocumentation from "./useDocumentation";
export { useDocumentation };

View File

@ -0,0 +1,10 @@
const useDocumentation = () => {
const openDocumentation = event => {
const url = process.env.REVERSE_PROXY_DOCS_URL;
window.open(url, "_blank");
event.preventDefault();
};
return { openDocumentation };
};
export default useDocumentation;

View File

@ -11,6 +11,7 @@ import frontendSessionReducer from "../../features/frontendSession/reducer";
import snackbarReducer from "../../features/snackbar/reducer";
import chartsReducer from "../../features/charts/chartsReducer";
import chatbotReducer from "../../features/chatbot/reducer";
import systemReducer from "../../features/system/reducer";
const rootReducer = combineReducers({
frontendSession: frontendSessionReducer,
@ -20,6 +21,7 @@ const rootReducer = combineReducers({
options: optionsReducer,
releaseNotes: releaseNotesReducer,
charts: chartsReducer,
system: systemReducer,
snackbar: snackbarReducer,
bot: chatbotReducer,
ajaxCallsInProgress: ajaxStatusReducer

View File

@ -15,6 +15,10 @@ export default {
}
}
},
system: {
data: { loading: false, loaded: false },
version: { loading: false, loaded: false }
},
snackbar: {
message: null,
type: null