Rewrite using typescript, integrate the latest changes from @mwarning/openwrt-firmware-selector

This commit is contained in:
Sudhanshu Gautam 2021-01-10 19:56:13 +05:30
parent d5c4ea592a
commit ce4c36622b
47 changed files with 5269 additions and 5282 deletions

View file

@ -0,0 +1,197 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Link,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@material-ui/core';
import { Launch, CloudDownload } from '@material-ui/icons';
import Axios from 'axios';
import { isEqual } from 'lodash';
import { useTranslation } from 'react-i18next';
import { ProfilesEntity } from '../../../types/overview';
import { Profile, TitlesEntity } from '../../../types/profile';
import config from '../../../config';
type Props = {
selectedVersion: string;
selectedProfile: ProfilesEntity;
};
const getTitle = (title: TitlesEntity) => {
return title.title || `${title.vendor} ${title.model}`;
};
const profilesData: { [key: string]: Profile } = {};
const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedProfile }) => {
const [profile, setProfileData] = useState<Profile>();
const [working, toggleWorking] = useState<boolean>(true);
const { t } = useTranslation();
const getHelpKey = (type: string) => {
const lc = type.toLowerCase();
if (lc.includes('sysupgrade')) {
return 'sysupgrade-help';
}
if (lc.includes('factory') || lc === 'trx' || lc === 'chk') {
return 'factory-help';
}
if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
return 'kernel-help';
}
if (lc.includes('root')) {
return 'rootfs-help';
}
if (lc.includes('sdcard')) {
return 'sdcard-help';
}
if (lc.includes('tftp')) {
return 'tftp-help';
}
return 'other-help';
};
const getProfileData = useCallback(async () => {
let profileData = profilesData[selectedProfile.id];
toggleWorking(true);
if (!profileData) {
const response = await Axios.get<Profile>(
`${process.env.PUBLIC_URL}/data/${selectedVersion}/${selectedProfile.target}/${selectedProfile.id}.json`
);
profileData = response.data;
profilesData[selectedProfile.id] = profileData;
}
toggleWorking(false);
return profileData;
}, [selectedVersion, selectedProfile]);
useEffect(() => {
if (selectedVersion && selectedProfile) {
getProfileData().then((_profileData) => {
if (!isEqual(profile, _profileData)) setProfileData(_profileData);
});
}
}, [selectedVersion, selectedProfile, getProfileData, profile]);
if (working || !profile) return <CircularProgress />;
const buildAt = new Date(profile.build_at);
return (
<>
<Box paddingTop={3} paddingBottom={2}>
<Typography variant="h6" component="h1" align="left">
{t('tr-version-build')}
</Typography>
</Box>
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>{t('tr-model')}</TableCell>
<TableCell>{profile.titles?.map((title) => getTitle(title)).join(', ')}</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-target')}</TableCell>
<TableCell>{profile.target}</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-version')}</TableCell>
<TableCell>
{profile.version_number} ({profile.version_code})
</TableCell>
</TableRow>
<TableRow>
<TableCell>{t('tr-date')}</TableCell>
<TableCell>{buildAt.toLocaleString()}</TableCell>
</TableRow>
<TableRow>
<TableCell>Info</TableCell>
{profile.titles && (
<TableCell>
{profile.titles
.map<React.ReactNode>((title) => {
const titleString = getTitle(title);
const infoUrl = config.info_url
.replace('{title}', encodeURI(titleString))
.replace('{target}', profile.target)
.replace('{id}', profile.id)
.replace('{version}', profile.version_number);
return (
<Link href={infoUrl}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{profile.titles!.length > 1 && (
<Typography component="span">{titleString}</Typography>
)}
<Launch
style={{
marginLeft: 10,
verticalAlign: 'middle',
}}
/>
</Link>
);
})
.reduce((prev, curr) => [
prev,
<Box display="inline-block" marginRight={2} />,
curr,
])}
</TableCell>
)}
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box paddingTop={3} paddingBottom={2}>
<Typography variant="h6" component="h1" align="left">
{t('tr-downloads')}
</Typography>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Download link</TableCell>
<TableCell>Help Text</TableCell>
</TableRow>
</TableHead>
<TableBody>
{profile.images?.map((i) => (
<TableRow>
<TableCell>
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
{i.type}
</Button>
</TableCell>
<TableCell>
<Box p={1}>
<Typography>{t(`tr-${getHelpKey(i.type)}`)}</Typography>
<Typography variant="caption">sha256sum: {i.sha256}</Typography>
</Box>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
export default ProfileDetails;

View file

@ -0,0 +1,97 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { CircularProgress, TextField } from '@material-ui/core';
import { Autocomplete, AutocompleteRenderInputParams, FilterOptionsState } from '@material-ui/lab';
import Axios from 'axios';
import { isEqual, throttle } from 'lodash';
import { matchSorter } from 'match-sorter';
import { useTranslation } from 'react-i18next';
import { Overview, ProfilesEntity } from '../../../types/overview';
type Props = {
selectedVersion: string;
onProfileChange: (profile: ProfilesEntity) => void;
};
type SearchData = { value: ProfilesEntity; search: string; title: string };
const overviewData: { [key: string]: Overview } = {};
const SearchField: FunctionComponent<Props> = ({ selectedVersion, onProfileChange }) => {
const [searchData, setSearchData] = useState<SearchData[]>([]);
const [working, toggleWorking] = useState<boolean>(true);
const { t } = useTranslation();
const getSearchData = useCallback(async () => {
let overview = overviewData[selectedVersion];
const searchDataArray: SearchData[] = [];
toggleWorking(true);
if (!overview) {
const response = await Axios.get<Overview>(
`${process.env.PUBLIC_URL}/data/${selectedVersion}/overview.json`
);
overview = response.data;
overviewData[selectedVersion] = overview;
}
toggleWorking(false);
overview.profiles?.forEach((profile) => {
profile.titles?.forEach((title) => {
searchDataArray.push({
value: profile,
search: profile.id + title.title,
title: title.title || `${title.vendor} ${title.model}`,
});
});
});
return searchDataArray;
}, [selectedVersion]);
useEffect(() => {
getSearchData().then((_searchData) => {
if (!isEqual(_searchData, searchData)) setSearchData(_searchData);
});
}, [getSearchData, searchData, selectedVersion]);
const handleProfileSelect = (_: unknown, searchDataRow: SearchData | null) => {
if (!searchDataRow) return;
onProfileChange(searchDataRow.value);
};
const getOptionLabel = (option: SearchData) => option.title;
const renderInput = (params: AutocompleteRenderInputParams) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<TextField {...params} fullWidth variant="outlined" label={t('tr-model')} />
);
const filterOptions: (
options: SearchData[],
state: FilterOptionsState<SearchData>
) => SearchData[] = (options, { inputValue }) =>
throttle(
() =>
matchSorter(options, inputValue.replaceAll(' ', ''), {
keys: ['search'],
}).slice(0, 10),
1000
)() || [];
if (working) return <CircularProgress />;
return (
<Autocomplete
options={searchData}
getOptionLabel={getOptionLabel}
renderInput={renderInput}
filterOptions={filterOptions}
onChange={handleProfileSelect}
/>
);
};
export default SearchField;

View file

@ -0,0 +1,40 @@
import React, { FunctionComponent } from 'react';
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import config from '../../../config';
type Props = {
selectedVersion: string;
onVersionChange: (version: string) => void;
};
const VersionSelector: FunctionComponent<Props> = ({ selectedVersion, onVersionChange }) => {
const { versions } = config;
const { t } = useTranslation();
const handleVersionChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const version: string = event.target.value as string;
onVersionChange(version);
};
return (
<FormControl fullWidth variant="outlined">
<InputLabel id="version-select-label">{t('tr-version')}</InputLabel>
<Select
labelWidth={60}
labelId="version-select-label"
value={selectedVersion}
onChange={handleVersionChange}
>
{Object.keys(versions).map((version) => (
<MenuItem value={version} key={version}>
{version}
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default VersionSelector;

View file

@ -1,895 +0,0 @@
import React from 'react';
import {
AppBar,
Button,
Chip,
CircularProgress,
ClickAwayListener,
Container,
FormControl,
Grid,
Input,
InputLabel,
List,
ListItem,
ListItemText,
MenuItem,
OutlinedInput,
Paper,
Select,
Tab,
Tabs,
TextField,
Tooltip,
Typography,
ExpansionPanel,
ExpansionPanelSummary,
ExpansionPanelDetails,
} from '@material-ui/core';
import CloudDownloadIcon from '@material-ui/icons/CloudDownload';
import WarningIcon from '@material-ui/icons/Warning';
import BuildIcon from '@material-ui/icons/Build';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import './home.scss';
import { withTranslation } from 'react-i18next';
import FuzzySet from 'fuzzyset.js';
import config from '../../config';
import DataService from '../../services/data';
import AlertDialog from '../../components/alert-dialog';
import ErrorSnackBar from '../../components/error-snackbar';
import SearchTextField from '../../components/device-search';
import PropTypes from 'prop-types';
const buildStatusCheckInterval = 5000;
const confirmationPopupOnBuildResquest = false;
function TabContainer({ children, dir }) {
return (
<Typography component="div" dir={dir} style={{ padding: '20px 0 0' }}>
{children}
</Typography>
);
}
TabContainer.propTypes = {
children: PropTypes.any,
dir: PropTypes.any,
};
const sleep = m => new Promise(r => setTimeout(r, m));
const { CORSbyPass, asu, asu_vanilla } = config;
class Home extends React.Component {
state = {
selection: {
version: null,
device: null,
},
showDeviceData: false,
deviceLoaded: false,
data: [],
devicesLoaded: false,
searchResults: [],
showSearch: false,
selectedSearchIndex: 0,
query: '',
downloading: false,
packages: this.packages,
configChanged: true,
packageName: '',
builtDeviceManifest: [],
builtImages: [],
isBuilding: false,
queuePosition: -1,
showUnexpectedErrorBar: false,
errorMessage: '',
fuzzySet: null,
showAdvanced: true,
basicInterface: 0,
errorDialogMessage: <></>,
openErrorDialog: false,
uciDefaults: '',
};
confirmingBuild = false;
dataService = new DataService();
async componentDidMount() {
try {
const versionsResponse = await this.dataService.getVersions(
CORSbyPass + asu_vanilla + 'versions.json'
);
let data = versionsResponse.data.versions;
for (var i = 0; i < data.length; i++) {
const overviewResponse = await this.dataService.getOverview(
CORSbyPass + asu_vanilla + data[i].path + '/overview.json'
);
data[i].devices = overviewResponse.data.devices;
}
this.generateFuzzySet(data[0].devices);
this.setState({
data,
selection: {
version: 0,
},
devicesLoaded: true,
});
} catch (err) {
this.setState({
showUnexpectedErrorBar: true,
});
console.log(err);
}
}
closeUnexpectedErrorBar = () => {
this.setState({
showUnexpectedErrorBar: false,
});
};
generateFuzzySet = data => {
let deviceNames = [];
Object.keys(data).forEach(deviceName => {
deviceNames.push(deviceName);
});
this.setState({
fuzzySet: FuzzySet(deviceNames),
});
};
setRelease = event => {
this.generateFuzzySet(this.state.data[event.target.value].devices);
this.setState({
selection: {
version: event.target.value,
},
deviceLoaded: false,
showDeviceData: false,
query: '',
});
};
selectDevice = async device_name => {
const version = this.state.data[this.state.selection.version];
let selection;
try {
let deviceSubPath = version.devices[device_name];
if (deviceSubPath.indexOf('//') > 0) {
deviceSubPath = deviceSubPath.replace('//', '/generic/');
}
const devicePath = version.path + '/targets/' + deviceSubPath;
this.setState({
showDeviceData: true,
showSearch: false,
query: device_name,
basicInterface: 0,
deviceLoaded: false,
showAdvanced: false,
configChanged: true,
});
const deviceResponse = await this.dataService.getDeviceData(
CORSbyPass + asu_vanilla + devicePath
);
selection = this.state.selection;
selection.device = deviceResponse.data;
if (selection.device.target[selection.device.target.length - 1] === '/') {
selection.device.target += 'generic';
}
selection.device.deviceManifest = await this.dataService.getDeviceManifest(
CORSbyPass +
asu_vanilla +
version.path +
'/targets/' +
selection.device.target +
'/openwrt-' +
selection.device.target.split('/')[0] +
'-' +
selection.device.target.split('/')[1] +
'-default.manifest'
);
} catch (err) {
this.setState({
showUnexpectedErrorBar: true,
});
console.log(err);
return;
}
const noPackageFoundError = { error: 'no-packages-found' };
try {
let devicePackagesResponse = await this.dataService.getDevicePackages(
version.name,
selection.device.target,
selection.device.id
);
if (devicePackagesResponse.data.length === 0) {
throw noPackageFoundError;
}
var packages = devicePackagesResponse.data;
packages.sort();
this.setState({
packages,
showAdvanced: true,
});
} catch (err) {
console.log(err);
this.setState({
showAdvanced: false,
});
}
this.setState({
selection,
deviceLoaded: true,
});
};
search = event => {
const query = event.target.value;
this.setState({
query,
searchResults: [],
showSearch: false,
});
const deviceNames = this.state.fuzzySet.get(query, undefined, 0);
let searchResults = [];
if (deviceNames != null) {
for (let i = 0; i < deviceNames.length && i < 6; i++) {
searchResults.push(deviceNames[i][1]);
}
}
this.setState({
searchResults,
showSearch: query.length > 0,
});
};
hideSearchResults = () => {
this.setState({
showSearch: false,
});
};
changeInterface = (e, val) => {
this.setState({
basicInterface: val,
});
};
downloadingImageIndicatorShow = () => {
this.setState({
downloading: true,
});
setTimeout(() => {
this.setState({
downloading: false,
});
}, 2000);
};
changeAddPackageInput = event => {
this.setState({
packageName: event.target.value,
});
};
deletePackage = i => {
let packages = this.state.packages;
packages.splice(i, 1);
this.setState({
packages,
configChanged: true,
});
};
addPackage = event => {
if ((event.which || event.keyCode) === 13 && !event.shiftKey) {
let packages = this.state.packages;
const packageArray = this.state.packageName.split(/[,\n]+/);
packageArray.forEach(package_name => {
package_name = package_name.replace(' ', '');
if (package_name !== '' && packages.indexOf(package_name) === -1) {
packages.push(package_name);
}
});
this.setState({
packages,
packageName: '',
configChanged: true,
});
}
};
uciDefaultsEdit = event => {
this.setState({
uciDefaults: event.target.value,
configChanged: true,
});
};
closeConfirmBuildDialog = () => {
this.confirmingBuild = false;
};
openConfirmBuildDialog = () => {
this.confirmingBuild = true;
};
displayBuiltImageData = async buildStatusResponse => {
const builtDeviceManifest = await this.dataService.getDeviceManifest(
CORSbyPass +
asu +
buildStatusResponse.data.image_folder +
'/' +
buildStatusResponse.data.image_prefix +
'.manifest'
);
let builtImages = [];
buildStatusResponse.data.images.forEach(image => {
builtImages.push({
url: asu + buildStatusResponse.data.image_folder + '/' + image.name,
type: image.type,
});
});
if (this.state.isBuilding) {
this.setState({
builtDeviceManifest,
builtImages,
configChanged: false,
isBuilding: false,
});
}
};
buildImageCheck = async request_hash => {
try {
if (!this.state.isBuilding) {
return;
}
const buildStatusResponse = await this.dataService.buildStatusCheck(
request_hash
);
if (buildStatusResponse.status === 202) {
if (
buildStatusResponse.headers['X-Build-Queue-Position'] !== undefined
) {
this.setState({
queuePosition:
buildStatusResponse.headers['X-Build-Queue-Position'],
});
}
await sleep(buildStatusCheckInterval);
await this.buildImageCheck(request_hash);
} else if (buildStatusResponse.status === 200) {
await this.displayBuiltImageData(buildStatusResponse);
} else if (buildStatusResponse.status === 409) {
this.setState({
openErrorDialog: true,
errorDialogMessage: (
<>
{buildStatusResponse.data.error} <br />
<a href={buildStatusResponse.data.error}>Build logs</a>
</>
),
});
} else {
throw buildStatusResponse.data;
}
} catch (e) {
if (e.response.status === 409) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: (
<>
{e.response.data.error} <br />
<a
href={asu + e.response.data.log}
target="_blank"
rel="noopener noreferrer"
>
Build logs
</a>
</>
),
});
} else if (e.response.status === 422) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: <>{e.response.data.error}</>,
});
} else {
this.setState({
isBuilding: false,
showUnexpectedErrorBar: true,
});
}
}
};
buildImage = async () => {
try {
this.closeConfirmBuildDialog();
const { packages, selection, data, uciDefaults } = this.state;
const {
device: { id: board, target },
version: versionId,
} = selection;
let {
[versionId]: { name: version },
} = data;
version = version.toLowerCase();
this.setState({
isBuilding: true,
builtImages: [],
});
let buildResponse = await this.dataService.buildImage(
board,
packages,
target,
version,
uciDefaults
);
if (
buildResponse.status === 202 &&
buildResponse.data['request_hash'] !== undefined
) {
const request_hash = buildResponse.data['request_hash'];
await sleep(buildStatusCheckInterval);
await this.buildImageCheck(request_hash);
} else if (buildResponse.status === 200) {
await this.displayBuiltImageData(buildResponse);
} else {
throw buildResponse.data;
}
} catch (e) {
console.log(e);
if (e.response.status === 409) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: (
<>
{e.response.data.error} <br />
<a
href={asu + e.response.data.log}
target="_blank"
rel="noopener noreferrer"
>
Build logs
</a>
</>
),
});
} else if (e.response.status === 422) {
this.setState({
isBuilding: false,
openErrorDialog: true,
errorDialogMessage: <>{e.response.data.error}</>,
});
} else {
this.setState({
isBuilding: false,
showUnexpectedErrorBar: true,
});
}
}
};
cancelBuild = () => {
this.setState({
isBuilding: false,
configChanged: true,
});
};
closeErrorDialog = () => {
this.setState({
openErrorDialog: false,
});
};
render() {
let warning432 = <> </>;
if (
this.state.showDeviceData &&
this.state.deviceLoaded &&
parseInt(
(this.state.selection.device['image_size'] || '').slice(0, -1)
) <= 4000
) {
warning432 = (
<Paper className="warning-432" elevation={0}>
<Grid container direction="row" justify="center" alignItems="center">
<Grid item>
<WarningIcon className="icon" />
</Grid>
<Grid item xs>
{this.props.t('warning432')}
</Grid>
</Grid>
</Paper>
);
}
let deviceInfoGrid = <></>;
if (this.state.builtImages.length > 0 && !this.state.configChanged) {
deviceInfoGrid = (
<Grid container className="device-info">
<Grid item xs>
{this.props.t('Model')}:{' '}
<b> {this.state.selection.device['title']} </b> <br />
{this.props.t('Target')}: {this.state.selection.device['target']}{' '}
<br />
{this.props.t('Version')}: {'('}
{this.state.data[this.state.selection.version].name}
{this.state.data[this.state.selection.version].revision}
{')'}
<ExpansionPanel className="installed-packages" elevation={0}>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
id="packages-manifest"
>
<Typography className="installed-packages-title">
Installed Packages ({this.state.builtDeviceManifest.length})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div>
{this.state.builtDeviceManifest.map(package_name => (
<div key={package_name}>{package_name}</div>
))}
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Grid>
<Grid item xs className="downloads">
<b>{this.props.t('Downloads')}: </b>
{this.state.builtImages.map(image => (
<div key={image.url}>
<Button
className="download-button"
href={image.url}
color="primary"
variant="contained"
onClick={() => this.downloadingImageIndicatorShow()}
>
<CloudDownloadIcon className="download-icon" />
{image.type}
</Button>
</div>
))}
&nbsp;
{this.state.downloading && <CircularProgress size={20} />}
</Grid>
</Grid>
);
}
const notLoaded = <CircularProgress />;
const onLoad = (
<>
<Typography variant="h5">
{this.props.t('Download OpenWrt firmware for your device!')}
</Typography>
<Typography>
{this.props.t(
'Please use the input below to download firmware for your device!'
)}
</Typography>
<br />
<ClickAwayListener onClickAway={this.hideSearchResults}>
<div className="search-container">
<FormControl className="version-select">
<InputLabel htmlFor="version-select" className="version-label">
{this.props.t('Version')}
</InputLabel>
<Select
value={this.state.selection.version}
onChange={this.setRelease}
disabled={this.state.isBuilding}
input={
<OutlinedInput
name="version"
id="version-select"
labelWidth={60}
/>
}
>
{this.state.data.map((version, i) => (
<MenuItem value={i} key={version.revision}>
<em>{version.name}</em>
</MenuItem>
))}
</Select>
</FormControl>
<FormControl className="search-field">
<SearchTextField
id="outlined-adornment-search-devices"
labeltext={this.props.t('Search your device')}
value={this.state.query}
onChange={this.search}
onClick={this.search}
disabled={this.state.isBuilding}
/>
{this.state.showSearch && this.state.searchResults.length !== 0 && (
<Paper elevation={4} className="search-results">
<List>
{this.state.searchResults.map(res => {
return (
<ListItem
key={res}
button
onClick={() => this.selectDevice(res)}
>
<ListItemText primary={<div>{res}</div>} />
</ListItem>
);
})}
</List>
</Paper>
)}
{this.state.searchResults.length === 0 && this.state.showSearch && (
<Paper elevation={4} className="search-results">
<ListItem>
<ListItemText primary={this.props.t('No results')} />
</ListItem>
</Paper>
)}
</FormControl>
</div>
</ClickAwayListener>
{this.state.showDeviceData && !this.state.deviceLoaded && (
<>
<br />
{notLoaded}
</>
)}
{this.state.showDeviceData && this.state.deviceLoaded && (
<>
{warning432}
<br />
{this.state.showAdvanced && (
<AppBar
className="interface-switch-bar"
position="relative"
elevation={0}
>
<Tabs
value={this.state.basicInterface}
onChange={this.changeInterface}
>
<Tab
className="interface-switch"
label={this.props.t('Basic')}
disabled={this.state.isBuilding}
/>
<Tab
className="interface-switch"
label={this.props.t('Advanced')}
disabled={this.state.isBuilding}
/>
</Tabs>
</AppBar>
)}
{this.state.basicInterface === 0 ? (
<TabContainer>
<Grid container className="device-info">
<Grid item xs>
{this.props.t('Model')}:{' '}
<b> {this.state.selection.device['title']} </b> <br />
{this.props.t('Target')}:{' '}
{this.state.selection.device['target']} <br />
{this.props.t('Version')}:{' '}
{this.state.data[this.state.selection.version].name} (
{this.state.data[this.state.selection.version].revision})
<ExpansionPanel
className="installed-packages"
elevation={0}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
id="packages-manifest"
>
<Typography className="installed-packages-title">
Installed Packages (
{this.state.selection.device.deviceManifest.length})
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className="packages">
<div>
{this.state.selection.device.deviceManifest.map(
package_name => (
<div key={package_name}>{package_name}</div>
)
)}
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Grid>
<Grid item xs className="downloads">
<b>{this.props.t('Downloads')}: </b>
{this.state.selection.device.images.map(image => (
<div key={image.name}>
<Button
className="download-button"
href={
asu_vanilla +
this.state.data[this.state.selection.version].path +
'/targets/' +
this.state.selection.device.target +
'/' +
image.name
}
color="primary"
variant="contained"
onClick={() => this.downloadingImageIndicatorShow()}
>
<CloudDownloadIcon className="download-icon" />
{
image.name
.split('-')
.reverse()[0]
.split('.')[0]
}
</Button>
</div>
))}
&nbsp;
{this.state.downloading && <CircularProgress size={20} />}
</Grid>
</Grid>
</TabContainer>
) : (
<TabContainer>
<Paper elevation={0} className="package-list-input">
<Grid container>
<Grid item xs={12} md={8}>
<h3>Packages</h3>
{this.state.packages.map((package_name, i) => (
<Chip
className="package"
key={package_name + i}
size="small"
onDelete={() => this.deletePackage(i)}
label={package_name}
/>
))}
<Tooltip
title={
<span>
Use comma or new line separated array. <br />
Press enter to apply.
</span>
}
>
<Input
multiline
value={this.state.packageName}
onKeyUp={this.addPackage}
onChange={this.changeAddPackageInput}
placeholder={this.props.t('Add package(s)')}
/>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<h3>UCI defaults (alpha)</h3>
<TextField
style={{ width: '100%' }}
multiline
variant="outlined"
rows={10}
value={this.state.uciDefaults}
onChange={this.uciDefaultsEdit}
placeholder={this.props.t('Edit UCI defaults')}
/>
</Grid>
</Grid>
<br />
{this.state.configChanged && !this.state.isBuilding && (
<Button
variant="outlined"
color="primary"
onClick={
confirmationPopupOnBuildResquest
? this.openConfirmBuildDialog
: this.buildImage
}
>
<BuildIcon />
&nbsp;
{this.props.t('Build')}
</Button>
)}
{this.state.isBuilding && (
<>
<Button
variant="outlined"
size="small"
onClick={this.cancelBuild}
>
&nbsp;
{this.props.t('Cancel')}
</Button>
&nbsp; &nbsp;
<CircularProgress
size={20}
style={{ verticalAlign: 'middle' }}
/>
&nbsp; Building image &nbsp;
{this.state.queuePosition !== -1 && (
<span>
{' '}
(Position in queue: {this.state.queuePosition}){' '}
</span>
)}
...
</>
)}
{deviceInfoGrid}
</Paper>
</TabContainer>
)}
</>
)}
</>
);
return (
<>
<ErrorSnackBar
open={this.state.showUnexpectedErrorBar}
closeHandle={this.closeUnexpectedErrorBar}
/>
<AlertDialog
cancelHandler={this.closeConfirmBuildDialog}
acceptHandler={this.buildImage}
open={this.confirmingBuild}
body={
<>
{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 />
</>
}
/>
<AlertDialog
cancelHandler={this.closeErrorDialog}
open={this.state.openErrorDialog}
body={this.state.errorDialogMessage}
title={this.props.t(
'There is an error with the packages you selected'
)}
cancelComponent={this.props.t('Dismiss')}
/>
<Container className="home-container">
<Paper className="home-container-paper">
{this.state.devicesLoaded ? onLoad : notLoaded}
</Paper>
</Container>
</>
);
}
}
Home.propTypes = {
t: PropTypes.func,
};
export default withTranslation()(Home);

View file

@ -1,188 +0,0 @@
$bg-color: #f0f0f0;
.home-container {
margin-top: 30px;
margin-bottom: 100px;
@media all and (max-width: 820px) {
margin-top: 15px;
margin-bottom: 100px;
}
.home-container-paper {
padding: 30px;
text-align: left;
@media all and (max-width: 820px) {
padding: 15px;
}
.warning-432 {
background-color: $bg-color;
padding: 10px;
margin-top: 20px;
border: 1px solid #e3e3e3;
color: #666;
.icon {
margin: 0 20px 0 10px;
color: #f9a825;
}
}
.device-info {
margin-bottom: 15px;
.installed-packages {
width: 80%;
border: 1px solid #e3e3e3;
border-radius: 7px;
margin-top: 16px !important;
@media all and (max-width: 820px) {
width: 100%;
}
}
.installed-packages .packages {
overflow: auto;
word-wrap: none;
white-space: nowrap;
}
.installed-packages::before {
display: none;
}
.installed-packages-title {
font-weight: bold;
}
}
.version-select {
width: 200px;
.version-label {
background-color: inherit;
padding: 5px;
margin-top: -10px;
margin-left: 10px;
display: block;
z-index: 10;
}
@media all and (max-width: 820px) {
width: 100%;
margin: 0;
padding: 0;
}
}
.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;
}
@media all and (max-width: 820px) {
width: 100%;
margin: 20px 0 0;
padding: 0;
}
}
.MuiTypography-h4 {
font-weight: bold;
margin-bottom: 15px;
}
.downloads {
@media all and (max-width: 820px) {
margin-top: 20px;
}
.download-button {
margin-top: 17px;
.download-icon {
margin-right: 10px;
}
}
}
.interface-switch-bar {
border: 1px solid #e3e3e3;
border-radius: 4px;
background-color: #fff;
z-index: 9;
overflow: hidden;
.interface-switch {
color: #000;
}
.advanced-settings {
border: 1px solid #999;
background-color: $bg-color;
border-radius: 6px;
margin-top: 20px;
overflow: hidden;
margin-bottom: 20px;
.icon {
font-size: 1em;
vertical-align: text-top;
}
.options {
padding-top: 30px;
background-color: #fff;
}
}
}
.package-list-input {
.package {
margin: 5px 10px 5px 0px;
border-radius: 30px;
vertical-align: middle;
user-select: none;
}
.package:hover {
background-color: $bg-color;
}
.package:focus,
.package:active {
transition: 0.2s;
background-color: darken($bg-color, 15%);
}
}
}
}
.device-table {
margin-top: 20px;
width: 100%;
td {
padding: 20px 30px;
}
tr:nth-child(odd) {
background-color: rgba(104, 74, 238, 0.07);
}
}

View file

@ -0,0 +1,64 @@
import React, { FunctionComponent, useState } from 'react';
import { Container, Paper, Box, Typography, Grid } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import SearchField from './components/SearchField';
import VersionSelector from './components/VersionSelector';
import ProfileDetails from './components/ProfileDetails';
import config from '../../config';
import { ProfilesEntity } from '../../types/overview';
const Home: FunctionComponent = () => {
const [selectedVersion, setSelectedVersion] = useState(Object.keys(config.versions)[0]);
const [selectedProfile, setSelectedProfile] = useState<ProfilesEntity | null>();
const { t } = useTranslation();
const onVersionChange = (version: string) => {
setSelectedVersion(version);
};
const onProfileChange = (profile: ProfilesEntity) => {
setSelectedProfile(profile);
};
return (
<Container>
<Box paddingY={4}>
<Paper>
<Box padding={3}>
<Box paddingBottom={2}>
<Typography variant="h4" component="h1" align="left">
{t('tr-load')}
</Typography>
</Box>
<Box paddingBottom={2}>
<Typography variant="h6" component="h2" align="left">
{t('tr-message')}
</Typography>
</Box>
<Grid container spacing={2}>
<Grid item xs>
<SearchField selectedVersion={selectedVersion} onProfileChange={onProfileChange} />
</Grid>
<Grid item xs={3}>
<VersionSelector
selectedVersion={selectedVersion}
onVersionChange={onVersionChange}
/>
</Grid>
</Grid>
{selectedProfile && (
<Box>
<ProfileDetails
selectedProfile={selectedProfile}
selectedVersion={selectedVersion}
/>
</Box>
)}
</Box>
</Paper>
</Box>
</Container>
);
};
export default Home;

View file

@ -1,15 +1,15 @@
import React from 'react';
import React, { FunctionComponent } from 'react';
import { Container, Paper, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
const page404Styles = makeStyles(theme => ({
const page404Styles = makeStyles((theme) => ({
root: {
padding: theme.spacing(3, 2),
},
}));
export default function NotFound() {
var classes = page404Styles();
const NotFound: FunctionComponent = () => {
const classes = page404Styles();
return (
<Container style={{ marginTop: '50px' }}>
<Paper className={classes.root} elevation={3}>
@ -20,4 +20,6 @@ export default function NotFound() {
</Paper>
</Container>
);
}
};
export default NotFound;