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 <me@sudhanshug.com>
This commit is contained in:
Sudhanshu Gautam 2019-07-21 20:49:13 +05:30
parent c05ac4dc2c
commit d30cf925b1
5 changed files with 424 additions and 90 deletions

View file

@ -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",

View file

@ -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 (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={open}
autoHideDuration={6000}
onClose={closeHandle}
ContentProps={{
'aria-describedby': 'message-id',
}}
>
<SnackbarContent
className={classes.error}
aria-describedby="client-snackbar"
message={
<span id="client-snackbar" className={classes.message}>
<ErrorIcon className={classes.icon}/>
{errorMessage ||
'An unexpected error occurred. Please try again'}
</span>
}
action={[
<IconButton key="close" aria-label="Close" color="inherit"
onClick={closeHandle}>
<CloseIcon/>
</IconButton>,
]}
/>
</Snackbar>
);
}
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 (
<Dialog
open={open}
onClose={() => handleClose(-1)}
onClose={cancelHandler}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle
id="alert-dialog-title">{t(title)}</DialogTitle>
id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{t(text)}
{text}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => handleClose(1)} color="primary">
{t('Build')} &nbsp; <BuildIcon/>
<Button onClick={acceptHandler} color="primary">
{acceptComponent}
</Button>
<Button onClick={() => handleClose(0)} color="secondary"
<Button onClick={cancelHandler} color="secondary"
variant="contained" autoFocus>
{t('Cancel')}
{cancelComponent}
</Button>
</DialogActions>
</Dialog>
@ -146,6 +209,7 @@ class Home extends React.Component {
'luci'];
deviceNames = [];
deviceNamesID = {};
checkBuildStatus;
state = {
showDeviceData: false,
device: {},
@ -158,24 +222,35 @@ 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.dataService.getDistributions.then(distros => {
this.setState({
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;
});
@ -183,11 +258,23 @@ class Home extends React.Component {
this.setState({
devices: data['devices'],
devicesLoaded: true,
release_version_number: data['version_number'],
});
});
});
}
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 {
<br/>
<ClickAwayListener onClickAway={this.hideSearchResults}>
<div className="search-container">
<FormControl fullWidth>
<FormControl className="version-select">
<InputLabel htmlFor="version-select" className="version-label">
{this.props.t('Version')}
</InputLabel>
<Select
value={this.state.release}
onChange={this.setRelease}
input={<OutlinedInput name="version"
id="version-select" labelWidth={60}/>}
>
{
Object.keys(this.state.distributions['versions'])
.map((version) => (
<MenuItem value={version} key={version}>
<em>{version}</em>
</MenuItem>
),
)
}
</Select>
</FormControl>
<FormControl className="search-field">
<SearchTextField
id="outlined-adornment-search-devices"
labeltext={this.props.t('Search your device')}
@ -331,9 +518,9 @@ class Home extends React.Component {
onChange={this.search}
onClick={this.search}
/>
</FormControl>
{
this.state.showSearch && (
this.state.showSearch && this.state.searchResults.length !==
0 && (
<Paper elevation={4} className="search-results">
<List>
{
@ -368,6 +555,7 @@ class Home extends React.Component {
</Paper>
)
}
</FormControl>
</div>
</ClickAwayListener>
{
@ -443,6 +631,7 @@ class Home extends React.Component {
return (
<Chip className="package"
key={package_name + i}
size="small"
onDelete={() => this.deletePackage(
i)}
label={package_name}
@ -462,12 +651,49 @@ class Home extends React.Component {
</Tooltip>
</div>
<br/>
{
this.state.configChanged && !this.state.isBuilding && (
<Button variant="outlined" color="primary"
onClick={this.openConfirmBuildDialog}>
<BuildIcon/>
&nbsp;
{this.props.t('Build')}
</Button>
)
}
{
this.state.isBuilding && (
<CircularProgress size={20}/>
)
}
{
this.state.builtImages.length > 0 && !this.state.configChanged && (
<>
{
this.state.builtImages.map((image) => (
<Button
key={image.url}
className="download-button"
href={image.url}
color="primary"
variant="contained"
onClick={() => this.downloadingImageIndicatorShow()}
>
<CloudDownloadIcon
className="download-icon"/>
{image.type}
</Button>
))
}
&nbsp;
{
this.state.downloading && (
<CircularProgress size={20}/>
)
}
</>
)
}
</Paper>
</TabContainer>
)
@ -478,16 +704,32 @@ class Home extends React.Component {
</>
);
return (
<>
<ErrorSnackBar
open={this.state.showUnexpectedErrorBar}
closeHandle={this.closeUnexpectedErrorBar}
/>
<AlertDialog
cancelHandler={this.closeConfirmBuildDialog}
acceptHandler={this.buildImage}
open={this.confirmingBuild}
text={this.props.t(
'Building image requires computation resources, so we would request you to check if this selection is what you want')}
title={this.props.t(
'Please confirm that you want to perform this action')}
cancelComponent={this.props.t('Cancel')}
acceptComponent={
<>
{this.props.t('Build')} &nbsp; <BuildIcon/>
</>
}
/>
<Container className="home-container">
<Paper className="home-container-paper">
<AlertDialog handleClose={this.closeConfirmBuildDialog}
open={this.confirmingBuild}
text="Building image requires computation resources, so we would request you to check if this selection is what you want"
title="Please confirm that you want to perform this action"
t={this.props.t}/>
{this.state.devicesLoaded ? onLoad : notLoaded}
</Paper>
</Container>
</>
);
}
}

View file

@ -22,6 +22,24 @@
margin-bottom: 15px;
}
.version-select {
width: 200px;
.version-label {
background-color: inherit;
padding: 5px;
margin-top: -10px;
margin-left: 10px;
display: block;
z-index: 10;
}
}
.search-field {
width: calc(100% - 220px);
margin-left: 20px;
position: relative;
.search-label {
background-color: #fff;
padding: 0 10px;
@ -31,8 +49,12 @@
.search-results {
position: absolute;
left: 0;
width: 100%;
top: 100%;
z-index: 10;
}
}
.MuiTypography-h4 {
font-weight: bold;

47
src/services/data.js Normal file
View file

@ -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;

View file

@ -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==