diff --git a/package.json b/package.json index 3322b17..317829f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@material-ui/core": "^4.1.2", "@material-ui/icons": "^4.2.1", + "axios": "^0.19.0", "fuzzyset.js": "^0.0.8", "gh-pages": "^2.0.1", "i18next": "^17.0.4", diff --git a/src/containers/home/home.js b/src/containers/home/home.js index f68af42..c49b05f 100644 --- a/src/containers/home/home.js +++ b/src/containers/home/home.js @@ -13,12 +13,19 @@ import { DialogTitle, FormControl, Grid, + IconButton, Input, InputAdornment, + InputLabel, List, ListItem, ListItemText, + MenuItem, + OutlinedInput, Paper, + Select, + Snackbar, + SnackbarContent, Tab, Tabs, TextField, @@ -31,10 +38,16 @@ import SearchIcon from '@material-ui/icons/Search'; import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; import WarningIcon from '@material-ui/icons/Warning'; import BuildIcon from '@material-ui/icons/Build'; +import CloseIcon from '@material-ui/icons/Close'; +import ErrorIcon from '@material-ui/icons/Error'; import './home.scss'; import {withTranslation} from 'react-i18next'; import FuzzySet from 'fuzzyset.js'; +import DataService from '../../services/data'; + +const buildStatusCheckInterval = 5000; + const useStylesSearch = makeStyles(theme => ({ root: { borderColor: '#e2e2e1', @@ -54,6 +67,56 @@ const useStylesSearch = makeStyles(theme => ({ focused: {}, })); +const SnackBarStyles = makeStyles(theme => ({ + error: { + backgroundColor: theme.palette.error.dark, + }, + message: { + display: 'flex', + alignItems: 'center', + }, + icon: { + marginRight: '20px', + fontSize: 20, + }, +})); + +function ErrorSnackBar({open, closeHandle, errorMessage}) { + const classes = SnackBarStyles(); + return ( + + + + {errorMessage || + 'An unexpected error occurred. Please try again'} + + } + action={[ + + + , + ]} + /> + + ); +} + function SearchTextField(props) { const classes = useStylesSearch(); @@ -86,28 +149,28 @@ function TabContainer({children, dir}) { ); } -function AlertDialog({open, handleClose, text, title, t}) { +function AlertDialog({open, cancelHandler, acceptHandler, text, title, cancelComponent, acceptComponent}) { return ( handleClose(-1)} + onClose={cancelHandler} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > {t(title)} + id="alert-dialog-title">{title} - {t(text)} + {text} - - @@ -146,6 +209,7 @@ class Home extends React.Component { 'luci']; deviceNames = []; deviceNamesID = {}; + checkBuildStatus; state = { showDeviceData: false, device: {}, @@ -158,36 +222,59 @@ class Home extends React.Component { query: '', downloading: false, packages: this.packages, - release_version_number: '', + distributions: { + versions: {}, + }, + configChanged: true, packageName: '', + release: '', + builtImages: [], + isBuilding: false, + showUnexpectedErrorBar: false, }; + fuzzySet; basicInterface = 0; confirmingBuild = false; - getDevicesData = () => fetch( - 'https://chef.libremesh.org/download/json/devices.json') - .then(res => res.json()); - getDeviceData = (device_id) => fetch( - 'https://chef.libremesh.org/download/json/' + device_id + '.json') - .then(res => res.json()); + dataService = new DataService(); componentDidMount() { - this.getDevicesData().then(data => { - Object.keys(data['devices']).forEach((device_id) => { - const device_name = data['devices'][device_id]; - this.deviceNames.push(device_name); - this.deviceNamesID[device_name] = device_id; - }); - this.fuzzySet = FuzzySet(this.deviceNames); + this.dataService.getDistributions.then(distros => { this.setState({ - devices: data['devices'], - devicesLoaded: true, - release_version_number: data['version_number'], + distributions: distros['openwrt'], + release: distros['openwrt']['latest'], + }); + this.dataService.getDevicesData.then(data => { + Object.keys(data['devices']).forEach((device_name) => { + // const device_name = data['devices'][device_id]; + // this.deviceNames.push(device_name); + // this.deviceNamesID[device_name] = device_id; + const device_id = data['devices'][device_name]; + this.deviceNames.push(device_name); + this.deviceNamesID[device_name] = device_id; + }); + this.fuzzySet = FuzzySet(this.deviceNames); + this.setState({ + devices: data['devices'], + devicesLoaded: true, + }); }); }); } + closeUnexpectedErrorBar = () => { + this.setState({ + showUnexpectedErrorBar: false, + }); + }; + + setRelease = (event) => { + this.setState({ + release: event.target.value, + }); + }; + selectDevice = (device_name) => { if (device_name != null) { const device_id = this.deviceNamesID[device_name]; @@ -197,7 +284,7 @@ class Home extends React.Component { query: device_name, deviceLoaded: false, }); - this.getDeviceData(device_id).then(data => { + this.dataService.getDeviceData(device_id).then(data => { this.setState({ device: data, deviceLoaded: true, @@ -244,7 +331,7 @@ class Home extends React.Component { this.setState({ downloading: false, }); - }, 1000); + }, 2000); }; changeAddPackageInput = (event) => { @@ -258,6 +345,7 @@ class Home extends React.Component { packages.splice(i, 1); this.setState({ packages, + configChanged: true }); }; @@ -274,19 +362,97 @@ class Home extends React.Component { this.setState({ packages, packageName: '', + configChanged: true }); } }; closeConfirmBuildDialog = (v) => { this.confirmingBuild = false; - console.log(v); }; openConfirmBuildDialog = () => { this.confirmingBuild = true; }; + displayBuiltImageData = async (buildStatusResponse) => { + console.log(buildStatusResponse); + await this.dataService.getFiles(buildStatusResponse.data.files) + .then((fileListResponse) => { + let builtImages = []; + fileListResponse.forEach((file) => { + const suffix = file.name.substring(file.name.length - 4); + if (suffix === '.bin') { + const type = file.name.split('-').reverse()[0].split('.')[0]; + builtImages.push({ + url: 'https://chef.libremesh.org' + + buildStatusResponse.data.files + file.name, + type, + }); + } + }); + this.setState({ + builtImages, + configChanged: false, + isBuilding: false, + }); + }); + clearTimeout(this.checkBuildStatus); + }; + + buildImageCheck = async (request_hash) => { + const buildStatusResponse = await this.dataService.buildStatusCheck( + request_hash); + if (buildStatusResponse.status === 202) { + this.checkBuildStatus = setTimeout( + () => { + this.buildImageCheck(request_hash); + }, buildStatusCheckInterval, + ); + } else if (buildStatusResponse.status === 200) { + await this.displayBuiltImageData(buildStatusResponse); + } else { + this.setState({ + isBuilding: false, + showUnexpectedErrorBar: true, + }); + } + }; + + buildImage = async () => { + this.closeConfirmBuildDialog(); + const board = this.state.device.id; + const packages = this.state.packages; + const target = this.state.device.target + '/' + this.state.device.subtarget; + const version = this.state.release; + this.setState({ + isBuilding: true, + builtImages: [], + }); + this.dataService.buildImage(board, packages, target, version).then(async res => { + if (res.status === 202 && res.data['request_hash'] !== undefined) { + const request_hash = res.data['request_hash']; + this.checkBuildStatus = setTimeout( + async () => { + await this.buildImageCheck(request_hash); + }, buildStatusCheckInterval, + ); + } else if (res.status === 200) { + await this.displayBuiltImageData(res); + } else { + this.setState({ + isBuilding: false, + showUnexpectedErrorBar: true, + }); + } + }).catch(() => { + this.setState({ + isBuilding: false, + showUnexpectedErrorBar: true, + }); + }); + }; + render() { const warning432 = this.state.showDeviceData && parseInt( @@ -323,7 +489,28 @@ class Home extends React.Component {
- + + + {this.props.t('Version')} + + + + + { + this.state.showSearch && this.state.searchResults.length !== + 0 && ( + + + { + this.state.searchResults.map((res, index) => { + return ( + this.selectDevice(res)} + > + + {res} +
+ }/> + + ); + }) + } + + + ) + } + { + (this.state.searchResults.length === 0 && + this.state.showSearch) && ( + + + + + + ) + } - { - this.state.showSearch && ( - - - { - this.state.searchResults.map((res, index) => { - return ( - this.selectDevice(res)} - > - - {res} - - }/> - - ); - }) - } - - - ) - } - { - (this.state.searchResults.length === 0 && - this.state.showSearch) && ( - - - - - - ) - }
{ @@ -443,6 +631,7 @@ class Home extends React.Component { return ( this.deletePackage( i)} label={package_name} @@ -462,12 +651,49 @@ class Home extends React.Component {
- + { + this.state.configChanged && !this.state.isBuilding && ( + + ) + } + { + this.state.isBuilding && ( + + ) + } + { + this.state.builtImages.length > 0 && !this.state.configChanged && ( + <> + { + this.state.builtImages.map((image) => ( + + )) + } +   + { + this.state.downloading && ( + + ) + } + + ) + } ) @@ -478,16 +704,32 @@ class Home extends React.Component { ); return ( - - - - {this.state.devicesLoaded ? onLoad : notLoaded} - - + <> + + + {this.props.t('Build')}   + + } + /> + + + {this.state.devicesLoaded ? onLoad : notLoaded} + + + ); } } diff --git a/src/containers/home/home.scss b/src/containers/home/home.scss index 76ec9a0..8b41c41 100644 --- a/src/containers/home/home.scss +++ b/src/containers/home/home.scss @@ -22,16 +22,38 @@ margin-bottom: 15px; } - .search-label { - background-color: #fff; - padding: 0 10px; - position: absolute; - white-space: nowrap; + .version-select { + width: 200px; + + .version-label { + background-color: inherit; + padding: 5px; + margin-top: -10px; + margin-left: 10px; + display: block; + z-index: 10; + } } - .search-results { - position: absolute; - z-index: 10; + .search-field { + width: calc(100% - 220px); + margin-left: 20px; + position: relative; + + .search-label { + background-color: #fff; + padding: 0 10px; + position: absolute; + white-space: nowrap; + } + + .search-results { + position: absolute; + left: 0; + width: 100%; + top: 100%; + z-index: 10; + } } .MuiTypography-h4 { diff --git a/src/services/data.js b/src/services/data.js new file mode 100644 index 0000000..c1f6471 --- /dev/null +++ b/src/services/data.js @@ -0,0 +1,47 @@ +import axios from 'axios'; + +const base = 'https://cors-anywhere.herokuapp.com/https://mwarning.de/misc/json/bin'; + +class DataService { + getDevicesData = axios.get( + `${base}/overview.json`) + .then(res => res.data); + + getDeviceData = (device_id) => axios.get( + base + '/targets/' + device_id) + .then(res => res.data); + + getDistributions = axios.get( + 'https://chef.libremesh.org/api/distributions') + .then(res => res.data); + + buildImage = (board, packages, target, version) => { + return axios.post('https://chef.libremesh.org/api/build-request', { + board, + defaults: '', + distro: 'openwrt', + packages, + target, + version, + }); + }; + + buildStatusCheck = async (request_hash) => { + let response = { + isBuilt: false, + }; + await axios.get('https://chef.libremesh.org/api/build-request/' + request_hash).then((res) => { + response.isBuilt = res.status === 202 && res.data.files !== undefined; + response.status = res.status; + if (response.isBuilt) { + response = {...response, data: res.data} + } + }); + return response; + }; + + getFiles = (files_url) => axios.get('https://chef.libremesh.org' + files_url).then(res => res.data); + +} + +export default DataService; diff --git a/yarn.lock b/yarn.lock index 1de5b8b..93913b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,6 +1958,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + axobject-query@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -3266,6 +3274,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6. dependencies: ms "2.0.0" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -4263,6 +4278,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.7.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" @@ -5221,7 +5243,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0: +is-buffer@^2.0.0, is-buffer@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==