From d30cf925b1b9b80a66ca64cbb7a63d3ad5649006 Mon Sep 17 00:00:00 2001 From: Sudhanshu Gautam Date: Sun, 21 Jul 2019 20:49:13 +0530 Subject: [PATCH] Complete build functionality. Used mwarning's server for data. Now users can build their images using the Build button and it works fine for the most part. There is still some issue with the file download as there are a lot of them and will be imporved in the future. Using axios now for cross browser support issue of `fetch` Signed-off-by: Sudhanshu Gautam --- package.json | 1 + src/containers/home/home.js | 404 +++++++++++++++++++++++++++------- src/containers/home/home.scss | 38 +++- src/services/data.js | 47 ++++ yarn.lock | 24 +- 5 files changed, 424 insertions(+), 90 deletions(-) create mode 100644 src/services/data.js 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==