Allow features to be disabled at build time (#143)
* Add framework for built-time feature selection * Allow MQTT, NTP, OTA features to be disabled at build time * Allow Project screens to be disabled at build time * Allow security features to be disabled at build time * Switch to std::function for StatefulService function aliases for greater flexibility * Bump various UI lib versions * Update docs
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
# Change the IP address to that of your ESP device to enable local development of the UI.
|
||||
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
|
||||
REACT_APP_HTTP_ROOT=http://192.168.0.99
|
||||
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99
|
||||
REACT_APP_HTTP_ROOT=http://192.168.0.88
|
||||
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.88
|
||||
|
208
interface/package-lock.json
generated
208
interface/package-lock.json
generated
@ -1403,6 +1403,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"@npmcli/move-file": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz",
|
||||
"integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==",
|
||||
"requires": {
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
|
||||
@ -3324,11 +3339,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"classnames": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||
},
|
||||
"clean-css": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||
@ -3538,16 +3548,122 @@
|
||||
}
|
||||
},
|
||||
"compression-webpack-plugin": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz",
|
||||
"integrity": "sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-4.0.0.tgz",
|
||||
"integrity": "sha512-DRoFQNTkQ8gadlk117Y2wxANU+MDY56b1FIZj/yJXucBOTViTHXjthM7G9ocnitksk4kLzt1N2RLF0gDjxI+hg==",
|
||||
"requires": {
|
||||
"cacache": "^13.0.1",
|
||||
"find-cache-dir": "^3.0.0",
|
||||
"neo-async": "^2.5.0",
|
||||
"schema-utils": "^2.6.1",
|
||||
"serialize-javascript": "^2.1.2",
|
||||
"webpack-sources": "^1.0.1"
|
||||
"cacache": "^15.0.3",
|
||||
"find-cache-dir": "^3.3.1",
|
||||
"schema-utils": "^2.6.6",
|
||||
"serialize-javascript": "^3.0.0",
|
||||
"webpack-sources": "^1.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
|
||||
"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "15.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.4.tgz",
|
||||
"integrity": "sha512-YlnKQqTbD/6iyoJvEY3KJftjrdBYroCbxxYXzhOzsFLWlp6KX4BOlEf4mTx0cMUfVaTS3ENL2QtDWeRYoGLkkw==",
|
||||
"requires": {
|
||||
"@npmcli/move-file": "^1.0.1",
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"glob": "^7.1.4",
|
||||
"infer-owner": "^1.0.4",
|
||||
"lru-cache": "^5.1.1",
|
||||
"minipass": "^3.1.1",
|
||||
"minipass-collect": "^1.0.2",
|
||||
"minipass-flush": "^1.0.5",
|
||||
"minipass-pipeline": "^1.2.2",
|
||||
"mkdirp": "^1.0.3",
|
||||
"p-map": "^4.0.0",
|
||||
"promise-inflight": "^1.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ssri": "^8.0.0",
|
||||
"tar": "^6.0.2",
|
||||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
|
||||
"integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==",
|
||||
"requires": {
|
||||
"commondir": "^1.0.1",
|
||||
"make-dir": "^3.0.2",
|
||||
"pkg-dir": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"requires": {
|
||||
"semver": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"p-map": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||
"requires": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz",
|
||||
"integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==",
|
||||
"requires": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
|
||||
"integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
|
||||
"requires": {
|
||||
"minipass": "^3.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
@ -8122,6 +8238,15 @@
|
||||
"minipass": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz",
|
||||
"integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==",
|
||||
"requires": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
@ -8183,9 +8308,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz",
|
||||
"integrity": "sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
@ -8415,14 +8540,12 @@
|
||||
}
|
||||
},
|
||||
"notistack": {
|
||||
"version": "0.9.7",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-0.9.7.tgz",
|
||||
"integrity": "sha512-OztbtaIiCMR7QdcDGXTcYu96Uuvu26k41d7cnMGdf4NaKkAX06fsLPAlodGPj4HrMjMBUl8nvUQ3LmQOS6kHWQ==",
|
||||
"version": "0.9.16",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-0.9.16.tgz",
|
||||
"integrity": "sha512-+q1KKj2XkU+mKnbp9PbVkRLSLfVYnPJGi+MHT+N9Pm3nZUMVtbjDFodwdv/RoEldvkXKCROnecayUFMwLOiIQA==",
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.6"
|
||||
"clsx": "^1.1.0",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
@ -10227,9 +10350,9 @@
|
||||
}
|
||||
},
|
||||
"react-app-rewired": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.1.5.tgz",
|
||||
"integrity": "sha512-Gr8KfCeL9/PTQs8Vvxc7v8wQ9vCFMnYPhcAkrMlzkLiMFXS+BgSwm11MoERjZm7dpA2WjTi+Pvbu/w7rujAV+A==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.1.6.tgz",
|
||||
"integrity": "sha512-06flj0kK5tf/RN4naRv/sn6j3sQd7rsURoRLKLpffXDzJeNiAaTNic+0I8Basojy5WDwREkTqrMLewSAjcb13w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^5.6.0"
|
||||
@ -12099,6 +12222,31 @@
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
||||
"integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA=="
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz",
|
||||
"integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^3.0.0",
|
||||
"minizlib": "^2.1.0",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "4.6.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz",
|
||||
@ -12354,9 +12502,9 @@
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.7.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
|
||||
"integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw=="
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz",
|
||||
"integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ=="
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
"version": "1.0.4",
|
||||
|
@ -13,12 +13,12 @@
|
||||
"@types/react-material-ui-form-validator": "^2.0.5",
|
||||
"@types/react-router": "^5.1.3",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"compression-webpack-plugin": "^3.0.1",
|
||||
"compression-webpack-plugin": "^4.0.0",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"lodash": "^4.17.15",
|
||||
"mime-types": "^2.1.25",
|
||||
"moment": "^2.24.0",
|
||||
"notistack": "^0.9.7",
|
||||
"moment": "^2.26.0",
|
||||
"notistack": "^0.9.16",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-form-validator-core": "^0.6.4",
|
||||
@ -27,7 +27,7 @@
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.1",
|
||||
"sockette": "^2.0.6",
|
||||
"typescript": "^3.7.5",
|
||||
"typescript": "^3.9.5",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
@ -51,6 +51,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-app-rewired": "^2.1.5"
|
||||
"react-app-rewired": "^2.1.6"
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import CloseIcon from '@material-ui/icons/Close';
|
||||
import AppRouting from './AppRouting';
|
||||
import CustomMuiTheme from './CustomMuiTheme';
|
||||
import { PROJECT_NAME } from './api';
|
||||
import FeaturesWrapper from './features/FeaturesWrapper';
|
||||
|
||||
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
|
||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||
@ -34,10 +35,12 @@ class App extends Component {
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}>
|
||||
<Switch>
|
||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||
<Route component={AppRouting} />
|
||||
</Switch>
|
||||
<FeaturesWrapper>
|
||||
<Switch>
|
||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||
<Route component={AppRouting} />
|
||||
</Switch>
|
||||
</FeaturesWrapper>
|
||||
</SnackbarProvider>
|
||||
</CustomMuiTheme>
|
||||
);
|
||||
|
@ -16,30 +16,45 @@ import System from './system/System';
|
||||
|
||||
import { PROJECT_PATH } from './api';
|
||||
import Mqtt from './mqtt/Mqtt';
|
||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||
import { Features } from './features/types';
|
||||
|
||||
class AppRouting extends Component {
|
||||
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/wifi/";
|
||||
|
||||
class AppRouting extends Component<WithFeaturesProps> {
|
||||
|
||||
componentDidMount() {
|
||||
Authentication.clearLoginRedirect();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { features } = this.props;
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<Switch>
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
|
||||
{features.security && (
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
)}
|
||||
{features.project && (
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
)}
|
||||
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
|
||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||
{features.ntp && (
|
||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
|
||||
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
||||
<AuthenticatedRoute exact path="/system/*" component={System} />
|
||||
<Redirect to="/" />
|
||||
)}
|
||||
{features.mqtt && (
|
||||
<AuthenticatedRoute exact path="/mqtt/*" component={Mqtt} />
|
||||
)}
|
||||
{features.security && (
|
||||
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
||||
)}
|
||||
<AuthenticatedRoute exact path="/system/*" component={System} />
|
||||
<Redirect to={getDefaultRoute(features)} />
|
||||
</Switch>
|
||||
</AuthenticationWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouting;
|
||||
export default withFeatures(AppRouting);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ENDPOINT_ROOT } from './Env';
|
||||
|
||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
||||
|
@ -1,7 +1,8 @@
|
||||
import * as H from 'history';
|
||||
|
||||
import history from '../history';
|
||||
import { PROJECT_PATH } from '../api';
|
||||
import { Features } from '../features/types';
|
||||
import { getDefaultRoute } from '../AppRouting';
|
||||
|
||||
export const ACCESS_TOKEN = 'access_token';
|
||||
export const LOGIN_PATHNAME = 'loginPathname';
|
||||
@ -26,12 +27,12 @@ export function clearLoginRedirect() {
|
||||
getStorage().removeItem(LOGIN_SEARCH);
|
||||
}
|
||||
|
||||
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
||||
export function fetchLoginRedirect(features: Features): H.LocationDescriptorObject {
|
||||
const loginPathname = getStorage().getItem(LOGIN_PATHNAME);
|
||||
const loginSearch = getStorage().getItem(LOGIN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
pathname: loginPathname || `/${PROJECT_PATH}/`,
|
||||
pathname: loginPathname || getDefaultRoute(features),
|
||||
search: (loginPathname && loginSearch) || undefined
|
||||
};
|
||||
}
|
||||
|
@ -2,37 +2,21 @@ import * as React from 'react';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/styles';
|
||||
|
||||
import history from '../history'
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
||||
import { AuthenticationContext, Me } from './AuthenticationContext';
|
||||
import FullScreenLoading from '../components/FullScreenLoading';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
loadingPanel: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
flexDirection: "column"
|
||||
},
|
||||
progress: {
|
||||
margin: theme.spacing(4),
|
||||
}
|
||||
});
|
||||
|
||||
interface AuthenticationWrapperState {
|
||||
context: AuthenticationContext;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
type AuthenticationWrapperProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||
|
||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||
|
||||
@ -69,18 +53,16 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
}
|
||||
|
||||
renderContentLoading() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.loadingPanel}>
|
||||
<CircularProgress className={classes.progress} size={100} />
|
||||
<Typography variant="h4" >
|
||||
Loading...
|
||||
</Typography>
|
||||
</div>
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
if (!this.props.features.security) {
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||
return;
|
||||
}
|
||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
||||
if (accessToken) {
|
||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||
@ -124,4 +106,4 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(withSnackbar(AuthenticationWrapper))
|
||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
||||
|
@ -3,19 +3,21 @@ import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-d
|
||||
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
||||
import * as Authentication from './Authentication';
|
||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||
|
||||
interface UnauthenticatedRouteProps extends RouteProps {
|
||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps & AuthenticationContextProps> {
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||
|
||||
public render() {
|
||||
const { authenticationContext, component:Component, ...rest } = this.props;
|
||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
if (authenticationContext.me) {
|
||||
return (<Redirect to={Authentication.fetchLoginRedirect()} />);
|
||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
||||
}
|
||||
return (<Component {...props} />);
|
||||
}
|
||||
@ -25,4 +27,4 @@ class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps & Authenticat
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticationContext(UnauthenticatedRoute);
|
||||
export default withFeatures(withAuthenticationContext(UnauthenticatedRoute));
|
||||
|
57
interface/src/components/ApplicationError.tsx
Normal file
57
interface/src/components/ApplicationError.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { FC } from 'react';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
||||
import WarningIcon from "@material-ui/icons/Warning"
|
||||
|
||||
const styles = makeStyles(
|
||||
{
|
||||
siteErrorPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column"
|
||||
},
|
||||
siteErrorPagePanel: {
|
||||
textAlign: "center",
|
||||
padding: "280px 0 40px 0",
|
||||
backgroundImage: 'url("/app/icon.png")',
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% 40px",
|
||||
backgroundSize: "200px auto",
|
||||
width: "100%",
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
interface ApplicationErrorProps {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
||||
const classes = styles();
|
||||
return (
|
||||
<div className={classes.siteErrorPage}>
|
||||
<CssBaseline />
|
||||
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
||||
<Box display="flex" flexDirection="row" justifyContent="center">
|
||||
<WarningIcon fontSize="large" color="error" />
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Application error
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Failed to configure the application, please refresh to try again.
|
||||
</Typography>
|
||||
{error &&
|
||||
(
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Error: {error}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationError;
|
32
interface/src/components/FullScreenLoading.tsx
Normal file
32
interface/src/components/FullScreenLoading.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { Typography, Theme } from '@material-ui/core';
|
||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
||||
fullScreenLoading: {
|
||||
padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
flexDirection: "column"
|
||||
},
|
||||
progress: {
|
||||
margin: theme.spacing(4),
|
||||
}
|
||||
}));
|
||||
|
||||
const FullScreenLoading = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.fullScreenLoading}>
|
||||
<CircularProgress className={classes.progress} size={100} />
|
||||
<Typography variant="h4">
|
||||
Loading …
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FullScreenLoading;
|
@ -1,4 +1,4 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import React, { RefObject, Fragment } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
|
||||
@ -20,6 +20,7 @@ import MenuIcon from '@material-ui/icons/Menu';
|
||||
import ProjectMenu from '../project/ProjectMenu';
|
||||
import { PROJECT_NAME } from '../api';
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
||||
|
||||
const drawerWidth = 290;
|
||||
|
||||
@ -82,7 +83,7 @@ interface MenuAppBarState {
|
||||
authMenuOpen: boolean;
|
||||
}
|
||||
|
||||
interface MenuAppBarProps extends AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
||||
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
||||
sectionTitle: string;
|
||||
}
|
||||
|
||||
@ -114,7 +115,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, theme, children, sectionTitle, authenticatedContext } = this.props;
|
||||
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
|
||||
const { mobileOpen, authMenuOpen } = this.state;
|
||||
const path = this.props.match.url;
|
||||
const drawer = (
|
||||
@ -128,9 +129,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</Typography>
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
{features.project && (
|
||||
<Fragment>
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
</Fragment>
|
||||
)}
|
||||
<List>
|
||||
<ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
|
||||
<ListItemIcon>
|
||||
@ -144,24 +148,30 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Access Point" />
|
||||
</ListItem>
|
||||
{features.ntp && (
|
||||
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
|
||||
<ListItemIcon>
|
||||
<AccessTimeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Network Time" />
|
||||
</ListItem>
|
||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
||||
<ListItemIcon>
|
||||
<DeviceHubIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="MQTT" />
|
||||
</ListItem>
|
||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItemIcon>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Security" />
|
||||
</ListItem>
|
||||
)}
|
||||
{features.mqtt && (
|
||||
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
|
||||
<ListItemIcon>
|
||||
<DeviceHubIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="MQTT" />
|
||||
</ListItem>
|
||||
)}
|
||||
{features.security && (
|
||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItemIcon>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Security" />
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
@ -172,6 +182,42 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
</div>
|
||||
);
|
||||
|
||||
const userMenu = (
|
||||
<div>
|
||||
<IconButton
|
||||
ref={this.anchorRef}
|
||||
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={this.handleToggle}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
||||
<ClickAwayListener onClickAway={this.handleClose}>
|
||||
<Card id="menu-list-grow">
|
||||
<CardContent>
|
||||
<List disablePadding>
|
||||
<ListItem disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions className={classes.authMenuActions}>
|
||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="fixed" className={classes.appBar} elevation={0}>
|
||||
@ -188,39 +234,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
|
||||
{sectionTitle}
|
||||
</Typography>
|
||||
<div>
|
||||
<IconButton
|
||||
ref={this.anchorRef}
|
||||
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={this.handleToggle}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
|
||||
<ClickAwayListener onClickAway={this.handleClose}>
|
||||
<Card id="menu-list-grow">
|
||||
<CardContent>
|
||||
<List disablePadding>
|
||||
<ListItem disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions className={classes.authMenuActions}>
|
||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</div>
|
||||
{features.security && userMenu}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<nav className={classes.drawer}>
|
||||
@ -263,8 +277,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||
|
||||
export default withRouter(
|
||||
withTheme(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
withFeatures(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -100,8 +100,8 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
||||
|
||||
render() {
|
||||
return <RestController
|
||||
{...this.props as P}
|
||||
{...this.state}
|
||||
{...this.props as P}
|
||||
handleValueChange={this.handleValueChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
|
23
interface/src/features/ApplicationContext.tsx
Normal file
23
interface/src/features/ApplicationContext.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Features } from './types';
|
||||
|
||||
export interface ApplicationContext {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
const ApplicationContextDefaultValue = {} as ApplicationContext
|
||||
export const ApplicationContext = React.createContext(
|
||||
ApplicationContextDefaultValue
|
||||
);
|
||||
|
||||
export function withAuthenticatedContexApplicationContext<T extends ApplicationContext>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof ApplicationContext>> {
|
||||
render() {
|
||||
return (
|
||||
<ApplicationContext.Consumer>
|
||||
{authenticatedContext => <Component {...this.props as T} features={authenticatedContext} />}
|
||||
</ApplicationContext.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
27
interface/src/features/FeaturesContext.tsx
Normal file
27
interface/src/features/FeaturesContext.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Features } from './types';
|
||||
|
||||
export interface FeaturesContext {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
const FeaturesContextDefaultValue = {} as FeaturesContext
|
||||
export const FeaturesContext = React.createContext(
|
||||
FeaturesContextDefaultValue
|
||||
);
|
||||
|
||||
export interface WithFeaturesProps {
|
||||
features: Features;
|
||||
}
|
||||
|
||||
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
|
||||
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
||||
render() {
|
||||
return (
|
||||
<FeaturesContext.Consumer>
|
||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
||||
</FeaturesContext.Consumer>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
61
interface/src/features/FeaturesWrapper.tsx
Normal file
61
interface/src/features/FeaturesWrapper.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Features } from './types';
|
||||
import { FeaturesContext } from './FeaturesContext';
|
||||
import FullScreenLoading from '../components/FullScreenLoading';
|
||||
import ApplicationError from '../components/ApplicationError';
|
||||
import { FEATURES_ENDPOINT } from '../api';
|
||||
|
||||
interface FeaturesWrapperState {
|
||||
features?: Features;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
|
||||
state: FeaturesWrapperState = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchFeaturesDetails();
|
||||
}
|
||||
|
||||
fetchFeaturesDetails = () => {
|
||||
fetch(FEATURES_ENDPOINT)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error("Unexpected status code: " + response.status);
|
||||
}
|
||||
}).then(features => {
|
||||
this.setState({ features });
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ error: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { features, error } = this.state;
|
||||
if (features) {
|
||||
return (
|
||||
<FeaturesContext.Provider value={{
|
||||
features
|
||||
}}>
|
||||
{this.props.children}
|
||||
</FeaturesContext.Provider>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<ApplicationError error={error} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FeaturesWrapper;
|
7
interface/src/features/types.ts
Normal file
7
interface/src/features/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface Features {
|
||||
project: boolean;
|
||||
security: boolean;
|
||||
mqtt: boolean;
|
||||
ntp: boolean;
|
||||
ota: boolean;
|
||||
}
|
@ -8,8 +8,9 @@ import { MenuAppBar } from '../components';
|
||||
|
||||
import SystemStatusController from './SystemStatusController';
|
||||
import OTASettingsController from './OTASettingsController';
|
||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||
|
||||
type SystemProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps;
|
||||
|
||||
class System extends Component<SystemProps> {
|
||||
|
||||
@ -18,16 +19,20 @@ class System extends Component<SystemProps> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { authenticatedContext } = this.props;
|
||||
const { authenticatedContext, features } = this.props;
|
||||
return (
|
||||
<MenuAppBar sectionTitle="System">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value="/system/status" label="System Status" />
|
||||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
|
||||
{features.ota && (
|
||||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
|
||||
)}
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
|
||||
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
|
||||
{features.ota && (
|
||||
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
|
||||
)}
|
||||
<Redirect to="/system/status" />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
@ -35,4 +40,4 @@ class System extends Component<SystemProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(System);
|
||||
export default withFeatures(withAuthenticatedContext(System));
|
||||
|
Reference in New Issue
Block a user