mirror of
https://git.netzspielplatz.de/docker-multiarch/openwrt-firmware-selector.git
synced 2025-11-09 01:49:35 +00:00
Rewrite using typescript, integrate the latest changes from @mwarning/openwrt-firmware-selector
This commit is contained in:
parent
d5c4ea592a
commit
ce4c36622b
47 changed files with 5269 additions and 5282 deletions
197
src/containers/home/components/ProfileDetails.tsx
Normal file
197
src/containers/home/components/ProfileDetails.tsx
Normal 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;
|
||||
97
src/containers/home/components/SearchField.tsx
Normal file
97
src/containers/home/components/SearchField.tsx
Normal 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;
|
||||
40
src/containers/home/components/VersionSelector.tsx
Normal file
40
src/containers/home/components/VersionSelector.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
))}
|
||||
|
||||
{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 />
|
||||
|
||||
{this.props.t('Build')}
|
||||
</Button>
|
||||
)}
|
||||
{this.state.isBuilding && (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={this.cancelBuild}
|
||||
>
|
||||
|
||||
{this.props.t('Cancel')}
|
||||
</Button>
|
||||
|
||||
<CircularProgress
|
||||
size={20}
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
Building image
|
||||
{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')} <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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
64
src/containers/home/home.tsx
Normal file
64
src/containers/home/home.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue