diff --git a/Dockerfile b/Dockerfile index 5503910..c825a18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 / diff --git a/config.js b/config.js index 31f4240..673851e 100644 --- a/config.js +++ b/config.js @@ -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", diff --git a/private/Notes.txt b/private/Notes.txt index 0d37fe1..825919f 100644 --- a/private/Notes.txt +++ b/private/Notes.txt @@ -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; - - -import { useTranslation } from 'react-i18next'; - -function MyComponent() { - const { t, i18n } = useTranslation(); +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 { 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 \ No newline at end of file +https://www.flaticon.com/free-icon/wizard_2534554?term=wizard&page=1&position=64 +https://lucasbassetti.com.br/react-simple-chatbot/#/docs/previous-value \ No newline at end of file diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index bf7d07b..35bd876 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -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:", diff --git a/public/locales/ro/translations.json b/public/locales/ro/translations.json index d9417a0..a867974 100644 --- a/public/locales/ro/translations.json +++ b/public/locales/ro/translations.json @@ -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:", diff --git a/src/components/home/HomePage.js b/src/components/home/HomePage.js index 9c8c7db..0c2407b 100644 --- a/src/components/home/HomePage.js +++ b/src/components/home/HomePage.js @@ -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 = () => {

+
+
+ +
); }; diff --git a/src/features/about/components/AboutContainer.js b/src/features/about/components/AboutContainer.js index d8f8ee8..f915f7e 100644 --- a/src/features/about/components/AboutContainer.js +++ b/src/features/about/components/AboutContainer.js @@ -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 ( <> - +

diff --git a/src/features/server/actionCreators.js b/src/features/server/actionCreators.js index 909f1d2..96d505e 100644 --- a/src/features/server/actionCreators.js +++ b/src/features/server/actionCreators.js @@ -3,7 +3,7 @@ import api from "./api"; import { sendHttpRequest } from "../../redux/actions/httpActions"; export function loadServerData() { - return async function (dispatch) { + return async function(dispatch) { try { const data = await dispatch(sendHttpRequest(api.getServerData())); dispatch({ type: types.LOAD_SERVER_DATA_SUCCESS, payload: data }); @@ -13,19 +13,8 @@ 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) { + return async function(dispatch) { try { const data = await dispatch(sendHttpRequest(api.getActiveSession())); dispatch({ type: types.LOAD_ACTIVE_SESSION_SUCCESS, payload: data }); diff --git a/src/features/server/actionTypes.js b/src/features/server/actionTypes.js index 0993767..f1c17be 100644 --- a/src/features/server/actionTypes.js +++ b/src/features/server/actionTypes.js @@ -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"; diff --git a/src/features/server/api.js b/src/features/server/api.js index c4a5c5c..08423bb 100644 --- a/src/features/server/api.js +++ b/src/features/server/api.js @@ -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 }; diff --git a/src/features/server/components/ServerComponent.js b/src/features/server/components/ServerComponent.js index 4c8663a..c18dc1b 100644 --- a/src/features/server/components/ServerComponent.js +++ b/src/features/server/components/ServerComponent.js @@ -65,7 +65,7 @@ const ServerComponent = ({ open={Boolean(anchorEl)} onClose={handleClose} > - {t("Server.Details")} + {t("Server.About")} { 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) }; } diff --git a/src/features/server/components/ServerSummary.js b/src/features/server/components/ServerSummary.js index 86c8a90..6bcdf38 100644 --- a/src/features/server/components/ServerSummary.js +++ b/src/features/server/components/ServerSummary.js @@ -24,13 +24,13 @@ const ServerSummary = ({ - {`${t("Server.ServerHostName")}: `} + {`${t("Server.HostName")}: `} {serverHost || ""} - {`${t("Server.ApiHostName")}: `} - {data.hosts.api} + {`${t("Server.SessionsCount")}: `} + {data.sessionsCount} diff --git a/src/features/server/reducer.js b/src/features/server/reducer.js index b769c68..f8428d0 100644 --- a/src/features/server/reducer.js +++ b/src/features/server/reducer.js @@ -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, diff --git a/src/features/system/actionCreators.js b/src/features/system/actionCreators.js new file mode 100644 index 0000000..55bf232 --- /dev/null +++ b/src/features/system/actionCreators.js @@ -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; + } + }; +} diff --git a/src/features/system/actionTypes.js b/src/features/system/actionTypes.js new file mode 100644 index 0000000..7b10717 --- /dev/null +++ b/src/features/system/actionTypes.js @@ -0,0 +1,2 @@ +export const LOAD_SYSTEM_DATA_SUCCESS = "LOAD_SYSTEM_DATA_SUCCESS"; +export const LOAD_SYSTEM_VERSION_SUCCESS = "LOAD_SYSTEM_VERSION_SUCCESS"; diff --git a/src/features/system/api.js b/src/features/system/api.js new file mode 100644 index 0000000..cd98c65 --- /dev/null +++ b/src/features/system/api.js @@ -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 +}; diff --git a/src/features/system/components/SystemComponent.js b/src/features/system/components/SystemComponent.js new file mode 100644 index 0000000..952a2ef --- /dev/null +++ b/src/features/system/components/SystemComponent.js @@ -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 ( + <> + + + {t("Menu.About")} + + + {t("Menu.Sessions")} + + + {t("Menu.ReleaseNotes")} + + + + + + + } + action={ + + + + } + title={{t("System.Title")}} + subheader={t("System.Subtitle")} + /> + + {data.loaded && } + + + + + + + + + + + + + + + + + + + ); +}; + +SystemComponent.propTypes = { + data: PropTypes.object.isRequired, + onRedirect: PropTypes.func.isRequired +}; + +export default SystemComponent; diff --git a/src/features/system/components/SystemContainer.js b/src/features/system/components/SystemContainer.js new file mode 100644 index 0000000..e95bddc --- /dev/null +++ b/src/features/system/components/SystemContainer.js @@ -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 ; +}; + +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)); diff --git a/src/features/system/components/SystemExtensionArea.js b/src/features/system/components/SystemExtensionArea.js new file mode 100644 index 0000000..ebbf7f8 --- /dev/null +++ b/src/features/system/components/SystemExtensionArea.js @@ -0,0 +1,8 @@ +import React from "react"; +import SystemVersionContainer from "./version/SystemVersionContainer"; + +const SystemExtensionArea = () => { + return ; +}; + +export default SystemExtensionArea; diff --git a/src/features/system/components/SystemSummary.js b/src/features/system/components/SystemSummary.js new file mode 100644 index 0000000..9db33a7 --- /dev/null +++ b/src/features/system/components/SystemSummary.js @@ -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 ( + + + {`${t("System.Server.HostName")}: `} + {data.server.hostName} + + + + {`${t("System.Server.Platform")}: `} + {data.server.platform} + + + + {`${t("System.Api.HostName")}: `} + {data.api.hostName} + + + + {`${t("System.Api.Platform")}: `} + {data.api.platform} + + + + + {t("System.Description")} + + + + ); +}; + +SystemSummary.propTypes = { + data: PropTypes.object.isRequired +}; + +export default SystemSummary; diff --git a/src/features/system/components/version/SystemVersionComponent.js b/src/features/system/components/version/SystemVersionComponent.js new file mode 100644 index 0000000..0c8d3b3 --- /dev/null +++ b/src/features/system/components/version/SystemVersionComponent.js @@ -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 ( + <> + + {t("System.Versions.Title")} + + + + + + + + + + {t("System.Versions.Server", { + version: data.server.version + })} + + } + secondary={t("System.LastUpdateDate", { + date: lastUpdateDate.server + })} + /> + + + + + + + + + {t("System.Versions.Api", { + version: data.api.version + })} + + } + secondary={t("System.LastUpdateDate", { + date: lastUpdateDate.api + })} + /> + + + + + + + + + {t("System.Versions.Frontend", { + version: process.env.APP_VERSION + })} + + } + secondary={t("System.LastUpdateDate", { + date: lastUpdateDate.frontend + })} + /> + + + + ); +}; + +SystemVersionComponent.propTypes = { + data: PropTypes.object.isRequired +}; + +export default SystemVersionComponent; diff --git a/src/features/system/components/version/SystemVersionContainer.js b/src/features/system/components/version/SystemVersionContainer.js new file mode 100644 index 0000000..46b91a4 --- /dev/null +++ b/src/features/system/components/version/SystemVersionContainer.js @@ -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 && }; +}; + +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); diff --git a/src/features/system/reducer.js b/src/features/system/reducer.js new file mode 100644 index 0000000..816c717 --- /dev/null +++ b/src/features/system/reducer.js @@ -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; + } +} diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..5a01219 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,3 @@ +import useDocumentation from "./useDocumentation"; + +export { useDocumentation }; diff --git a/src/hooks/useDocumentation.js b/src/hooks/useDocumentation.js new file mode 100644 index 0000000..cbb9c10 --- /dev/null +++ b/src/hooks/useDocumentation.js @@ -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; diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js index 280a9cf..339eda6 100644 --- a/src/redux/reducers/index.js +++ b/src/redux/reducers/index.js @@ -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 diff --git a/src/redux/reducers/initialState.js b/src/redux/reducers/initialState.js index b4cbb73..50cf0fc 100644 --- a/src/redux/reducers/initialState.js +++ b/src/redux/reducers/initialState.js @@ -15,6 +15,10 @@ export default { } } }, + system: { + data: { loading: false, loaded: false }, + version: { loading: false, loaded: false } + }, snackbar: { message: null, type: null