mirror of
https://git.netzspielplatz.de/docker-multiarch/openwrt-firmware-selector.git
synced 2025-11-08 18:59:27 +00:00
457 lines
16 KiB
TypeScript
457 lines
16 KiB
TypeScript
import React, { FunctionComponent, ReactNode, useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
CircularProgress,
|
|
FormControl,
|
|
Grid,
|
|
IconButton,
|
|
Input,
|
|
InputAdornment,
|
|
InputLabel,
|
|
Link,
|
|
makeStyles,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
} from '@material-ui/core';
|
|
import { Launch, CloudDownload, Add } from '@material-ui/icons';
|
|
import axios from 'axios';
|
|
import { isEqual } from 'lodash';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { DateTime } from 'luxon';
|
|
|
|
import { ProfilesEntity } from '../../../types/overview';
|
|
import { Profile, TitlesEntity } from '../../../types/profile';
|
|
import config from '../../../config';
|
|
import { getTitle } from '../utils/title';
|
|
import asu from '../../../utils/asu';
|
|
import { GetBuildResponse } from '../../../types/asu';
|
|
|
|
const useStyles = makeStyles(() => ({
|
|
chip: {
|
|
'&:focus': {
|
|
border: `2px solid #000`,
|
|
},
|
|
},
|
|
}));
|
|
|
|
type Props = {
|
|
selectedVersion: string;
|
|
selectedProfile: ProfilesEntity;
|
|
};
|
|
|
|
const profilesData: { [key: string]: Profile } = {};
|
|
|
|
const ProfileDetails: FunctionComponent<Props> = ({ selectedVersion, selectedProfile }) => {
|
|
const classes = useStyles();
|
|
const [profile, setProfile] = useState<Profile>();
|
|
const [showAddPackages, setShowAddPackages] = useState(false);
|
|
const [customPackages, setCustomPackages] = useState<Set<string>>(new Set());
|
|
const [customPackagesInputValue, setCustomPackagesInputValue] = useState<string>('');
|
|
const [showPackageExistsError, setShowPackageExistsError] = useState(false);
|
|
const [buildStatus, setBuildStatus] = useState<React.ReactNode>();
|
|
const [buildResponse, setBuildResponse] = useState<GetBuildResponse>();
|
|
const [buildError, setBuildError] = useState<string>();
|
|
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 preExistingPackages = (_profile = profile) =>
|
|
Array.from(
|
|
new Set([...(_profile?.default_packages || []), ...(_profile?.device_packages || [])])
|
|
);
|
|
|
|
const getProfileData = useCallback(async () => {
|
|
let profileData = profilesData[selectedProfile.id];
|
|
|
|
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;
|
|
}
|
|
|
|
return profileData;
|
|
}, [selectedVersion, selectedProfile]);
|
|
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
getProfileData().then((_profileData) => {
|
|
if (mounted && !isEqual(profile, _profileData)) {
|
|
setProfile(_profileData);
|
|
setCustomPackages(new Set(preExistingPackages(_profileData)));
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [selectedVersion, selectedProfile, getProfileData, profile]);
|
|
|
|
const toggleAddPackages = () => {
|
|
if (!profile) return;
|
|
setShowAddPackages(!showAddPackages);
|
|
setCustomPackages(new Set(preExistingPackages()));
|
|
};
|
|
|
|
const appendAddPackageInput = (
|
|
e?: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
if (((e && e.key === 'Enter') || !e) && customPackagesInputValue) {
|
|
setCustomPackages((prev) => {
|
|
setShowPackageExistsError(false);
|
|
if (
|
|
!profile?.device_packages?.includes(customPackagesInputValue) &&
|
|
!profile?.default_packages?.includes(customPackagesInputValue)
|
|
) {
|
|
return new Set(prev.add(customPackagesInputValue));
|
|
}
|
|
setShowPackageExistsError(true);
|
|
return prev;
|
|
});
|
|
setCustomPackagesInputValue('');
|
|
}
|
|
};
|
|
|
|
const onBuildStatusChange = (status: string) => {
|
|
setBuildStatus(status);
|
|
};
|
|
|
|
if (!profile) return <CircularProgress />;
|
|
|
|
const buildCustomImage = async () => {
|
|
setBuildStatus('Please wait...');
|
|
try {
|
|
const response = await asu.build(
|
|
Array.from(customPackages.values()),
|
|
profile.id,
|
|
profile.version_number,
|
|
onBuildStatusChange
|
|
);
|
|
setBuildResponse(response);
|
|
} catch (e) {
|
|
console.log(e);
|
|
setBuildError(e.response.data.message);
|
|
}
|
|
setBuildStatus(undefined);
|
|
};
|
|
|
|
const buildAt = DateTime.fromFormat(profile.build_at, 'yyyy-MM-dd TT').toLocaleString(
|
|
DateTime.DATETIME_MED
|
|
);
|
|
const hasAbilityToBuildCustomPackages = Object.keys(config).includes('asu_url');
|
|
const canBuild = !isEqual(Array.from(customPackages.values()), preExistingPackages());
|
|
|
|
return (
|
|
<>
|
|
<Box paddingTop={3} paddingBottom={2}>
|
|
<Typography variant="h5" component="h3" align="left">
|
|
{t('tr-version-build')}
|
|
</Typography>
|
|
</Box>
|
|
|
|
<TableContainer>
|
|
<Table>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell>{t('tr-model')}</TableCell>
|
|
<TableCell id="title">
|
|
{profile.titles?.map((title) => getTitle(title)).join(', ')}
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>{t('tr-target')}</TableCell>
|
|
<TableCell id="target">{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>
|
|
<TableCell>
|
|
{profile.titles
|
|
?.map<React.ReactNode>((title: TitlesEntity) => {
|
|
const titleString = getTitle(title);
|
|
const infoUrl = config.info_url.replace('{title}', encodeURI(titleString));
|
|
return (
|
|
<Link href={infoUrl} key={titleString}>
|
|
{/* 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((acc: ReactNode, curr: ReactNode, i: number) => [
|
|
acc,
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
<Box display="inline-block" marginRight={2} key={i} />,
|
|
curr,
|
|
])}
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
<Box paddingTop={3} paddingBottom={2}>
|
|
<Typography variant="h5" component="h3" 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) => {
|
|
const downloadURL = `${config.image_url
|
|
.replace('{target}', profile.target)
|
|
.replace('{version}', profile.version_number)}/${i.name}`;
|
|
return (
|
|
<TableRow key={downloadURL + i.type}>
|
|
<TableCell>
|
|
<Link href={downloadURL} target="_blank" data-testid="download_link">
|
|
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
|
|
{i.type}
|
|
</Button>
|
|
</Link>
|
|
</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>
|
|
<Box paddingTop={3} paddingBottom={2}>
|
|
<Typography variant="h5" component="h3" align="left">
|
|
{t('Packages')}
|
|
</Typography>
|
|
</Box>
|
|
{profile.default_packages && profile.default_packages.length > 0 && (
|
|
<Box mb={2}>
|
|
<Typography variant="h6" align="left">
|
|
{t('Default Packages')}
|
|
</Typography>
|
|
{profile.default_packages?.join(', ')}
|
|
</Box>
|
|
)}
|
|
{profile.device_packages && profile.device_packages.length > 0 && (
|
|
<Box mb={2}>
|
|
<Typography variant="h6" align="left">
|
|
{t('Device Packages')}
|
|
</Typography>
|
|
{profile.device_packages.join(', ')}
|
|
</Box>
|
|
)}
|
|
{hasAbilityToBuildCustomPackages && (
|
|
<Box>
|
|
{!showAddPackages && (
|
|
<Button variant="outlined" size="small" onClick={toggleAddPackages}>
|
|
customize packages
|
|
</Button>
|
|
)}
|
|
{showAddPackages && (
|
|
<>
|
|
<Typography variant="h6" align="left">
|
|
{t('Customize Packages')}
|
|
<Box display="inline-block" ml={2}>
|
|
<Link href="https://openwrt.org/packages/table/start" target="_blank">
|
|
<Typography variant="caption">find packages index on this page</Typography>
|
|
</Link>
|
|
</Box>
|
|
</Typography>
|
|
<br />
|
|
{Array.from(customPackages.values()).map((p) => {
|
|
const isDefaultPackage = profile.default_packages?.includes(p);
|
|
return (
|
|
<Box key={p} pr={1} pb={1} display="inline-block">
|
|
<Chip
|
|
size="small"
|
|
label={p}
|
|
color={isDefaultPackage ? 'default' : 'primary'}
|
|
className={classes.chip}
|
|
onDelete={
|
|
isDefaultPackage
|
|
? undefined
|
|
: () =>
|
|
setCustomPackages((prev) => {
|
|
const newSet = new Set(Array.from(prev.values()));
|
|
newSet.delete(p);
|
|
return newSet;
|
|
})
|
|
}
|
|
/>
|
|
</Box>
|
|
);
|
|
})}
|
|
<br />
|
|
<FormControl size="small">
|
|
<InputLabel style={{ fontSize: '0.9em' }}>Custom Package Name</InputLabel>
|
|
<Input
|
|
value={customPackagesInputValue}
|
|
onChange={(e) => e && setCustomPackagesInputValue(e.currentTarget.value)}
|
|
onKeyUp={appendAddPackageInput}
|
|
endAdornment={
|
|
<InputAdornment position="end">
|
|
<IconButton size="small" onClick={() => appendAddPackageInput()}>
|
|
<Add />
|
|
</IconButton>
|
|
</InputAdornment>
|
|
}
|
|
/>
|
|
</FormControl>
|
|
{showPackageExistsError && (
|
|
<Box pt={2}>
|
|
<Typography color="error" variant="caption" component="div">
|
|
This profile already includes this package. Please try a diffrent one
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
<Box mt={3}>
|
|
{!buildStatus && (
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
disabled={!canBuild}
|
|
onClick={buildCustomImage}
|
|
>
|
|
build customized image
|
|
</Button>
|
|
)}
|
|
{buildStatus && (
|
|
<>
|
|
<Grid
|
|
container
|
|
alignContent="center"
|
|
direction="row"
|
|
alignItems="center"
|
|
spacing={2}
|
|
>
|
|
{typeof buildStatus === 'string' && (
|
|
<Grid item>
|
|
<CircularProgress />
|
|
</Grid>
|
|
)}
|
|
<Grid item>{buildStatus}</Grid>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</>
|
|
)}
|
|
{buildError && <Typography color="error">{buildError}</Typography>}
|
|
{buildResponse && (
|
|
<Box mt={3}>
|
|
<Typography variant="h5">Built Image:</Typography>
|
|
<Table>
|
|
<TableBody>
|
|
<TableRow>
|
|
<TableCell>Build At</TableCell>
|
|
<TableCell id="title">
|
|
{DateTime.fromFormat(
|
|
buildResponse.build_at.substr(0, 25),
|
|
'ccc, dd MMM yyyy TT',
|
|
{
|
|
zone: buildResponse.build_at.substr(26),
|
|
setZone: true,
|
|
}
|
|
).toLocaleString(DateTime.DATETIME_MED)}
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
<br />
|
|
<br />
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Download link</TableCell>
|
|
<TableCell>Help Text</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{buildResponse.images?.map((i) => {
|
|
const downloadURL = `${config.image_url
|
|
.replace('{target}', profile.target)
|
|
.replace('{version}', profile.version_number)}/${i.name}`;
|
|
return (
|
|
<TableRow key={downloadURL + i.type}>
|
|
<TableCell>
|
|
<Link href={downloadURL} target="_blank" data-testid="download_link">
|
|
<Button endIcon={<CloudDownload />} variant="contained" color="primary">
|
|
{i.type}
|
|
</Button>
|
|
</Link>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box p={1}>
|
|
<Typography>{t(`tr-${getHelpKey(i.type)}`)}</Typography>
|
|
<Typography variant="caption">sha256sum: {i.sha256}</Typography>
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ProfileDetails;
|