@mui/material#AlertTitle TypeScript Examples
The following examples show how to use
@mui/material#AlertTitle.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: request-result.tsx From example with MIT License | 6 votes |
export function RequestResult({ result, completeRender }: IRequestResultProps<any>) {
switch (result.type) {
case "empty":
return null
case "error":
return <Alert severity="error" icon={<Icon icon={faExclamationCircle}/>}>
<AlertTitle>Request rejected</AlertTitle>
{result.error}
</Alert>
case "complete":
return <Box>
<Alert variant="outlined" severity="success" icon={<Icon icon={faCheck}/>}>
<AlertTitle>Request completed</AlertTitle>
{completeRender?.(result.data)}
</Alert>
</Box>
}
}
Example #2
Source File: index.tsx From yearn-watch-legacy with GNU Affero General Public License v3.0 | 6 votes |
ErrorAlert = (props: ErrorAlertProps) => {
const { message, details } = props;
let detailsLabel = details;
if (details && details instanceof Error) {
detailsLabel = sanitizeErrors(details.message);
} else if (details) {
detailsLabel = sanitizeErrors(details);
}
return (
<div>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
{message}{' '}
{detailsLabel && (
<React.Fragment>
— <strong>{detailsLabel}</strong>
</React.Fragment>
)}
</Alert>
</div>
);
}
Example #3
Source File: items-page.tsx From sdk with MIT License | 6 votes |
export function ItemsPage() {
const connection = useContext(ConnectorContext)
const { items, fetching, error } = useFetchItems(connection.sdk, connection.walletAddress)
return (
<Page header="My Items">
<CommentedBlock sx={{ my: 2 }} comment={<GetItemsComment/>}>
{
error && <CommentedBlock sx={{ my: 2 }}>
<Alert severity="error">
<AlertTitle>Items fetch error</AlertTitle>
{error.message || error.toString()}
</Alert>
</CommentedBlock>
}
{
fetching ? <Box sx={{
my: 4,
display: 'flex',
justifyContent: "center",
}}>
<CircularProgress/>
</Box> : ( items && <Box sx={{my: 2}}>
<ItemsList items={items}/>
</Box> )
}
</CommentedBlock>
</Page>
)
}
Example #4
Source File: unsupported-blockchain-warning.tsx From sdk with MIT License | 6 votes |
export function UnsupportedBlockchainWarning({ blockchain, message }: IUnsupportedBlockchainWarningProps) {
return <Alert severity="warning">
<AlertTitle>
{
blockchain ?
<>Unsupported blockchain: {blockchain}</> :
<>Wallet is not connected</>
}
</AlertTitle>
{message ?? "Page functionality is limited"}
</Alert>
}
Example #5
Source File: connection-status.tsx From example with MIT License | 6 votes |
export function ConnectionStatus() {
const connection = useContext(ConnectorContext)
switch (connection?.state.status) {
case "connected":
return <Alert severity="success" icon={<Icon icon={faLink}/>}>
<AlertTitle>Current Status: connected</AlertTitle>
Application is connected to wallet <Address
address={connection.state.connection.address}
trim={false}
/>
</Alert>
case "disconnected":
const error = connectionErrorMessage(connection?.state.error)
return <Alert severity="error" icon={<Icon icon={faLinkSlash}/>}>
<AlertTitle>Disconnected</AlertTitle>
Application currently not connected to any wallet
{ error && <Box sx={{ mt: 1 }}>Last attempt error: {error}</Box> }
</Alert>
case "connecting":
return <Alert severity="info">
<AlertTitle>Connecting...</AlertTitle>
Connection to wallet in process
</Alert>
case "initializing":
return <Alert severity="info">
<AlertTitle>Initializing...</AlertTitle>
Connector initialization
</Alert>
default:
return null
}
}
Example #6
Source File: unsupported-blockchain-warning.tsx From example with MIT License | 6 votes |
export function UnsupportedBlockchainWarning({ blockchain, message }: IUnsupportedBlockchainWarningProps) {
return <Alert severity="warning">
<AlertTitle>
{
blockchain ?
<>Unsupported blockchain: {blockchain}</> :
<>Wallet is not connected</>
}
</AlertTitle>
{message ?? "Page functionality is limited"}
</Alert>
}
Example #7
Source File: ImproveThisPageTag.tsx From frontend with MIT License | 6 votes |
export default function ImproveThisPageTag({ githubUrl, figmaUrl }: Props) {
const { t } = useTranslation()
if (!githubUrl && !figmaUrl) return null
return (
<Container maxWidth="sm">
<Box mt={8}>
<Alert variant="outlined" color="info" severity="info">
<AlertTitle>{t('improve-this-page')}</AlertTitle>
{githubUrl && (
<Button
href={githubUrl}
size="small"
variant="text"
target="_blank"
rel="noreferrer noopener"
startIcon={<GitHub fontSize="small" />}>
{t('github-link-text')}
</Button>
)}
{figmaUrl && (
<Button
href={figmaUrl}
size="small"
variant="text"
target="_blank"
rel="noreferrer noopener"
startIcon={<Web fontSize="small" />}>
{t('figma-link-text')}
</Button>
)}
</Alert>
</Box>
</Container>
)
}
Example #8
Source File: ToolpadApp.tsx From mui-toolpad with MIT License | 6 votes |
function AppError({ error }: FallbackProps) {
return (
<FullPageCentered>
<Alert severity="error">
<AlertTitle>Something went wrong</AlertTitle>
<pre>{error.stack}</pre>
</Alert>
</FullPageCentered>
);
}
Example #9
Source File: ErrorAlert.tsx From mui-toolpad with MIT License | 6 votes |
export default function ErrorAlert({ error }: ErrorAlertProps) {
const message: string =
typeof (error as any)?.message === 'string' ? (error as any).message : String(error);
const stack: string | null =
typeof (error as any)?.stack === 'string' ? (error as any).stack : null;
const [expanded, setExpanded] = React.useState(false);
const toggleExpanded = React.useCallback(() => setExpanded((actual) => !actual), []);
return (
<Alert
severity="error"
sx={{
// The content of the Alert doesn't overflow nicely
// TODO: does this need to go in core?
'& .MuiAlert-message': { minWidth: 0 },
}}
action={
stack ? (
<IconButton color="inherit" onClick={toggleExpanded}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
) : null
}
>
<AlertTitle>{message}</AlertTitle>
<Collapse in={expanded}>
<Box sx={{ overflow: 'auto' }}>
<pre>{stack}</pre>
</Box>
</Collapse>
</Alert>
);
}
Example #10
Source File: Banner.tsx From abrechnung with GNU Affero General Public License v3.0 | 6 votes |
export function Banner() {
const cfg = useRecoilValue(config);
if (cfg.error) {
return (
<Alert sx={{ borderRadius: 0 }} color="error">
{cfg.error}
</Alert>
);
}
return (
<>
{cfg.messages.map((message, idx) => (
<Alert key={idx} sx={{ borderRadius: 0 }} color={message.type}>
{message.title && <AlertTitle>{message.title}</AlertTitle>}
{message.body}
</Alert>
))}
</>
);
}
Example #11
Source File: PersonDialog.tsx From frontend with MIT License | 5 votes |
export default function PersonDialog({ label, type, onSubmit }: Props) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleClickOpen = () => setOpen(true)
const handleClose = () => setOpen(false)
return (
<>
<Button fullWidth variant="contained" color="info" onClick={handleClickOpen}>
{label}
</Button>
<Dialog
open={open}
onClose={(e, reason) => {
if (reason === 'backdropClick') return
handleClose()
}}
onBackdropClick={() => false}>
<DialogTitle>
{label}
<IconButton
aria-label="close"
onClick={handleClose}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: (theme) => theme.palette.grey[500],
}}>
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ mb: 2 }}>
{type === 'beneficiary' ? (
<Alert severity="info">
<AlertTitle>{t('campaigns:campaign.beneficiary.name')}</AlertTitle>
Лице, в чиято полза се организира кампанията. От юридическа гледна точка,
бенефициентът <strong>НЕ влиза</strong> във взаимоотношения с оператора при набиране
на средства в негова полза. Всички договори, изисквания, банкова сметка на
кампанията са на името на организатора. Възможно е бенефициентът по една кампания да
е и неговият организатор.
</Alert>
) : (
<Alert severity="warning">
<AlertTitle>{t('campaigns:campaign.coordinator.name')}</AlertTitle>
Организаторът е физическото или юридическо лице, с което се сключва договор за
набиране на средства, след като негова заявка за кампания е одобрена. Набраните
средства се прехвърлят в неговата банкова сметка, от него се изискват отчети за
разходените средства. Когато дадено лице иска да стане организатор на кампании,
преминава през процес на верификация, за да се избегнат измамите. Организаторът също
може да е и бенефициент по дадена кампания.
</Alert>
)}
</Box>
<PersonForm
{...type}
onSubmit={(...args) => {
onSubmit(...args)
handleClose()
}}
/>
</DialogContent>
</Dialog>
</>
)
}
Example #12
Source File: index.tsx From ExpressLRS-Configurator with GNU General Public License v3.0 | 5 votes |
BuildResponse: FunctionComponent<BuildResponseProps> = memo(
({ response, firmwareVersionData }) => {
// TODO: translations
const toTitle = (errorType: BuildFirmwareErrorType | undefined): string => {
if (errorType === null || errorType === undefined) {
return 'Error';
}
switch (errorType) {
case BuildFirmwareErrorType.GenericError:
return 'Error';
case BuildFirmwareErrorType.GitDependencyError:
return 'Git dependency error';
case BuildFirmwareErrorType.PythonDependencyError:
return 'Python dependency error';
case BuildFirmwareErrorType.PlatformioDependencyError:
return 'Platformio dependency error';
case BuildFirmwareErrorType.BuildError:
return 'Build error';
case BuildFirmwareErrorType.FlashError:
return 'Flash error';
case BuildFirmwareErrorType.TargetMismatch:
return 'The target you are trying to flash does not match the devices current target, if you are sure you want to do this, click Force Flash below';
default:
return '';
}
};
return (
<>
{response !== undefined && response.success && (
<Alert severity="success">Success!</Alert>
)}
{response !== undefined && !response.success && (
<Alert sx={styles.errorMessage} severity="error">
<AlertTitle>
{toTitle(
response?.errorType ?? BuildFirmwareErrorType.GenericError
)}
</AlertTitle>
<p>
An error has occured, see the above log for the exact error
message. If you have not already done so, visit{' '}
<DocumentationLink
firmwareVersion={firmwareVersionData}
url="https://www.expresslrs.org/{version}/"
>
Expresslrs.org
</DocumentationLink>{' '}
and read the{' '}
<DocumentationLink
firmwareVersion={firmwareVersionData}
url="https://www.expresslrs.org/{version}/quick-start/getting-started/"
>
Flashing Guide
</DocumentationLink>{' '}
for your particular device as well as the{' '}
<DocumentationLink
firmwareVersion={firmwareVersionData}
url="https://www.expresslrs.org/{version}/quick-start/troubleshooting/#flashingupdating"
>
Troubleshooting Guide
</DocumentationLink>
. If you are still having issues after reviewing the
documentation, please copy the build logs above to an online paste
site and post in the #help-and-support channel on the{' '}
<DocumentationLink
firmwareVersion={firmwareVersionData}
url="https://discord.gg/dS6ReFY"
>
ExpressLRS Discord
</DocumentationLink>{' '}
with a link to the logs and other relevant information like your
device, which flashing method you were using, and what steps you
have already taken to resolve the issue.
</p>
</Alert>
)}
</>
);
}
)
Example #13
Source File: index.tsx From Search-Next with GNU General Public License v3.0 | 5 votes |
Lab: React.FC<PageProps> = (props) => {
const { route } = props;
const history = useNavigate();
const location = useLocation();
const [list, setList] = React.useState<Router[]>([]);
React.useEffect(() => {
setList(route?.routes || []);
}, []);
return (
<div {...props}>
<Alert severity="info">
<AlertTitle>提示</AlertTitle>
实验室中的功能均处在开发中,不保证实际发布。
</Alert>
<ContentList>
{list
.filter((i) =>
i?.status && ['beta', 'process'].includes(i?.status)
? isBeta()
: true,
)
.map((i) => (
<ItemCard
key={i.path}
title={
<div className="flex items-center gap-1">
{i.title}
{i?.status === 'process' && (
<Chip
color="warning"
label={i?.status}
size="small"
variant="outlined"
/>
)}
</div>
}
icon={i.icon}
onClick={() => history(i.path)}
></ItemCard>
))}
</ContentList>
</div>
);
}
Example #14
Source File: RemoteUpdateDialog.tsx From airmessage-web with Apache License 2.0 | 4 votes |
/**
* A dialog that allows the user to update their server remotely
*/
export default function RemoteUpdateDialog(props: {
isOpen: boolean,
onDismiss: () => void,
update: ServerUpdateData,
}) {
const [isInstalling, setInstalling] = useState(false);
const installTimeout = useRef<any | undefined>(undefined);
const [errorDetails, setErrorDetails] = useState<{message: string, details?: string} | undefined>(undefined);
const remoteInstallable = props.update.remoteInstallable;
//Check if this server update introduces a newer server protocol than we support
const protocolCompatible = useMemo((): boolean => {
return compareVersions(ConnectionManager.targetCommVer, props.update.protocolRequirement) >= 0;
}, [props.update.protocolRequirement]);
const updateNotice = useMemo((): string => {
if(!remoteInstallable) {
return `This server update cannot be installed remotely.
Please check AirMessage Server on ${ConnectionManager.getServerComputerName()} for details.`;
} else if(!protocolCompatible) {
return `This server update requires a newer version of AirMessage for web than is currently running.
Please refresh the webpage to check for updates.`;
} else {
return `This will install the latest version of AirMessage Server on ${ConnectionManager.getServerComputerName()}.
You will lose access to messaging functionality while the update installs.
In case the installation fails, please make sure you have desktop access to this computer before installing.`;
}
}, [remoteInstallable, protocolCompatible]);
//Installs a remote update
const installUpdate = useCallback(() => {
//Install the update
setInstalling(true);
setErrorDetails(undefined);
ConnectionManager.installRemoteUpdate(props.update.id);
//Start the installation timeout
installTimeout.current = setTimeout(() => {
installTimeout.current = undefined;
//Show an error snackbar
setErrorDetails({message: remoteUpdateErrorCodeToDisplay(RemoteUpdateErrorCode.Timeout)});
}, 10 * 1000);
}, [setInstalling, setErrorDetails, props.update.id]);
//Register for update events
const propsOnDismiss = props.onDismiss;
useEffect(() => {
const connectionListener: ConnectionListener = {
onClose(reason: ConnectionErrorCode): void {
//Close the dialog
propsOnDismiss();
},
onConnecting(): void {},
onOpen(): void {}
};
ConnectionManager.addConnectionListener(connectionListener);
const updateListener: RemoteUpdateListener = {
onInitiate(): void {
//Cancel the timeout
if(installTimeout.current !== undefined) {
clearTimeout(installTimeout.current);
installTimeout.current = undefined;
}
},
onError(code: RemoteUpdateErrorCode, details?: string): void {
//Set the update as not installing
setInstalling(false);
//Show an error snackbar
setErrorDetails({message: remoteUpdateErrorCodeToDisplay(code), details});
},
};
ConnectionManager.addRemoteUpdateListener(updateListener);
return () => {
ConnectionManager.removeConnectionListener(connectionListener);
ConnectionManager.removeRemoteUpdateListener(updateListener);
};
}, [propsOnDismiss, setInstalling, setErrorDetails]);
return (
<Dialog
open={props.isOpen}
onClose={props.onDismiss}
fullWidth>
<DialogTitle>Server update</DialogTitle>
<DialogContent dividers>
<Stack>
<Typography variant="body1">AirMessage Server {props.update.version} is now available - you have {ConnectionManager.getServerSoftwareVersion()}</Typography>
<Box color="text.secondary" sx={{marginTop: 2, marginBottom: 2}}>
<Markdown markdown={props.update.notes} />
</Box>
<Typography variant="body1" paragraph>
{updateNotice}
</Typography>
{!isInstalling ? (<>
{remoteInstallable && (
protocolCompatible ? (
<Button
sx={{alignSelf: "flex-end"}}
variant="contained"
onClick={installUpdate}>
Install update
</Button>
) : (
<Button
sx={{alignSelf: "flex-end"}}
variant="contained"
onClick={() => window.location.reload()}>
Refresh
</Button>
)
)}
</>) : (<>
<Box sx={{paddingBottom: 2, paddingTop: 2}}>
<Typography variant="body1">Installing update…</Typography>
<LinearProgress
sx={{
marginTop: 1,
borderRadius: 8,
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 8
},
}} />
</Box>
</>)}
{errorDetails !== undefined && (
<Alert severity="error" sx={{marginTop: 2}}>
<AlertTitle>Failed to install update</AlertTitle>
{errorDetails.message}
</Alert>
)}
</Stack>
</DialogContent>
</Dialog>
);
}
Example #15
Source File: Balances.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function Balances({ group }) {
const theme: Theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
const history = useHistory();
const personalAccounts = useRecoilValue(personalAccountsSeenByUser(group.id));
const clearingAccounts = useRecoilValue(clearingAccountsSeenByUser(group.id));
const balances = useRecoilValue(accountBalances(group.id));
const [selectedTab, setSelectedTab] = useState("1");
const colorGreen = theme.palette.mode === "light" ? theme.palette.success.light : theme.palette.success.dark;
const colorRed = theme.palette.mode === "light" ? theme.palette.error.light : theme.palette.error.dark;
const colorGreenInverted = theme.palette.mode === "dark" ? theme.palette.success.light : theme.palette.success.dark;
const colorRedInverted = theme.palette.mode === "dark" ? theme.palette.error.light : theme.palette.error.dark;
useTitle(`${group.name} - Balances`);
const chartData = personalAccounts.map((account) => {
return {
name: account.name,
balance: balances[account.id].balance,
totalPaid: balances[account.id].totalPaid,
totalConsumed: balances[account.id].totalConsumed,
id: account.id,
};
});
const unbalancedClearingAccounts = clearingAccounts
.filter((account) => balances[account.id].balance !== 0)
.map((account) => {
return {
name: account.name,
id: account.id,
balance: balances[account.id].balance,
};
});
const chartHeight = Object.keys(balances).length * 30 + 100;
// TODO determine the rendered width of the account names and take the maximum
const yaxiswidth = isSmallScreen
? Math.max(Math.max(...personalAccounts.map((account) => account.name.length)), 20)
: Math.max(...personalAccounts.map((account) => account.name.length)) * 7 + 5;
const handleBarClick = (data, event) => {
const id = data.activePayload[0].payload.id;
history.push(`/groups/${group.id}/accounts/${id}`);
};
return (
<MobilePaper>
<TabContext value={selectedTab}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList onChange={(event, idx) => setSelectedTab(idx)} centered>
<Tab label="Chart" value="1" />
<Tab label="Table" value="2" />
</TabList>
</Box>
<TabPanel value="1" sx={{ padding: { xs: 1, md: 2 } }}>
{personalAccounts.length === 0 && <Alert severity="info">No Accounts</Alert>}
{unbalancedClearingAccounts.length !== 0 && (
<Alert severity="info">
<AlertTitle>Some Clearing Accounts have remaining balances.</AlertTitle>
{unbalancedClearingAccounts.map((account) => (
<Typography variant="body2" key={account.id} component="span">
<>{account.name}:</>
<Typography
variant="body2"
component="span"
sx={{ color: account.balance < 0 ? colorRedInverted : colorGreenInverted }}
>
{account.balance.toFixed(2)} {group.currency_symbol}{" "}
</Typography>
</Typography>
))}
</Alert>
)}
{isSmallScreen ? (
<List>
{personalAccounts.map((account) => (
<>
<ListItemLink key={account.id} to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText primary={account.name} />
<Typography
align="right"
variant="body2"
sx={{
color:
balances[account.id].balance < 0
? colorRedInverted
: colorGreenInverted,
}}
>
{balances[account.id].balance.toFixed(2)} {group.currency_symbol}
</Typography>
</ListItemLink>
<Divider key={account.id * 2} component="li" />
</>
))}
</List>
) : (
<div className="area-chart-wrapper" style={{ width: "100%", height: `${chartHeight}px` }}>
<ResponsiveContainer>
<BarChart
data={chartData}
margin={{
top: 20,
right: 20,
bottom: 20,
left: 20,
}}
layout="vertical"
onClick={handleBarClick}
>
<XAxis
stroke={theme.palette.text.primary}
type="number"
unit={group.currency_symbol}
/>
<YAxis
dataKey="name"
stroke={theme.palette.text.primary}
type="category"
width={yaxiswidth}
/>
<Tooltip
formatter={(label) =>
parseFloat(label).toFixed(2) + ` ${group.currency_symbol}`
}
labelStyle={{
color: theme.palette.text.primary,
}}
itemStyle={{
color: theme.palette.text.primary,
}}
contentStyle={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
borderRadius: theme.shape.borderRadius,
}}
/>
<Bar dataKey="balance">
{chartData.map((entry, index) => {
return (
<Cell
key={`cell-${index}`}
fill={entry["balance"] >= 0 ? colorGreen : colorRed}
/>
);
})}
<LabelList
dataKey={(entry) =>
`${entry["balance"].toFixed(2)}${group.currency_symbol}`
}
position="insideLeft"
fill={theme.palette.text.primary}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
</TabPanel>
<TabPanel value="2" sx={{ padding: { xs: 1, md: 2 } }}>
<BalanceTable group={group} />
</TabPanel>
</TabContext>
</MobilePaper>
);
}
Example #16
Source File: index.tsx From ExpressLRS-Configurator with GNU General Public License v3.0 | 4 votes |
ConfiguratorView: FunctionComponent<ConfiguratorViewProps> = (props) => {
const {
gitRepository,
selectedDevice,
networkDevices,
onDeviceChange,
deviceType,
} = props;
const [viewState, setViewState] = useState<ViewState>(
ViewState.Configuration
);
const { setAppStatus } = useAppState();
const [progressNotifications, setProgressNotifications] = useState<
BuildProgressNotification[]
>([]);
const progressNotificationsRef = useRef<BuildProgressNotification[]>([]);
const [
lastProgressNotification,
setLastProgressNotification,
] = useState<BuildProgressNotification | null>(null);
useBuildProgressNotificationsSubscription({
onSubscriptionData: (options) => {
const args = options.subscriptionData.data?.buildProgressNotifications;
if (args !== undefined) {
const newNotificationsList = [
...progressNotificationsRef.current,
args,
];
progressNotificationsRef.current = newNotificationsList;
setProgressNotifications(newNotificationsList);
setLastProgressNotification(args);
}
},
});
/*
We batch log events in order to save React.js state updates and rendering performance.
*/
const [logs, setLogs] = useState<string>('');
const logsRef = useRef<string[]>([]);
const eventsBatcherRef = useRef<EventsBatcher<string> | null>(null);
useEffect(() => {
eventsBatcherRef.current = new EventsBatcher<string>(200);
eventsBatcherRef.current.onBatch((newLogs) => {
const newLogsList = [...logsRef.current, ...newLogs];
logsRef.current = newLogsList;
setLogs(newLogsList.join(''));
});
}, []);
useBuildLogUpdatesSubscription({
fetchPolicy: 'network-only',
onSubscriptionData: (options) => {
const args = options.subscriptionData.data?.buildLogUpdates.data;
if (args !== undefined && eventsBatcherRef.current !== null) {
eventsBatcherRef.current.enqueue(args);
}
},
});
const [
firmwareVersionData,
setFirmwareVersionData,
] = useState<FirmwareVersionDataInput | null>(null);
const [firmwareVersionErrors, setFirmwareVersionErrors] = useState<Error[]>(
[]
);
const onFirmwareVersionData = useCallback(
(data: FirmwareVersionDataInput) => {
setFirmwareVersionErrors([]);
setFirmwareVersionData(data);
},
[]
);
const [deviceTarget, setDeviceTarget] = useState<Target | null>(null);
const [deviceTargetErrors, setDeviceTargetErrors] = useState<Error[]>([]);
const onDeviceTarget = useCallback(
(data: Target | null) => {
setDeviceTargetErrors([]);
setDeviceTarget(data);
// if target was manually changed, set selected device to null
onDeviceChange(null);
},
[onDeviceChange]
);
const [deviceTargets, setDeviceTargets] = useState<Device[] | null>(null);
const [
fetchDeviceTargets,
{
loading: loadingTargets,
data: targetsResponse,
error: targetsResponseError,
},
] = useAvailableFirmwareTargetsLazyQuery({
fetchPolicy: 'network-only',
});
const [
fetchLuaScript,
{ data: luaScriptResponse, error: luaScriptResponseError },
] = useLuaScriptLazyQuery();
const device = useMemo(() => {
return deviceTargets?.find((d) => {
return d.targets.find((target) => target.id === deviceTarget?.id);
});
}, [deviceTarget, deviceTargets]);
useEffect(() => {
if (
firmwareVersionData === null ||
validateFirmwareVersionData(firmwareVersionData).length > 0
) {
setDeviceTargets(null);
} else {
fetchDeviceTargets({
variables: {
source: firmwareVersionData.source as FirmwareSource,
gitBranch: firmwareVersionData.gitBranch!,
gitTag: firmwareVersionData.gitTag!,
gitCommit: firmwareVersionData.gitCommit!,
localPath: firmwareVersionData.localPath!,
gitPullRequest: firmwareVersionData.gitPullRequest,
gitRepository: {
url: gitRepository.url,
owner: gitRepository.owner,
repositoryName: gitRepository.repositoryName,
rawRepoUrl: gitRepository.rawRepoUrl,
srcFolder: gitRepository.srcFolder,
},
},
});
}
}, [gitRepository, firmwareVersionData, fetchDeviceTargets]);
useEffect(() => {
if (targetsResponse?.availableFirmwareTargets) {
setDeviceTargets([...targetsResponse.availableFirmwareTargets]);
} else {
setDeviceTargets(null);
}
}, [targetsResponse]);
const [
deviceOptionsFormData,
setDeviceOptionsFormData,
] = useState<DeviceOptionsFormData>({
userDefinesTxt: '',
userDefinesMode: UserDefinesMode.UserInterface,
userDefineOptions: [],
});
const handleDeviceOptionsResponse = async (
deviceOptionsResponse: TargetDeviceOptionsQuery
) => {
const storage = new ApplicationStorage();
const deviceName = device?.name || null;
const userDefineOptions = await mergeWithDeviceOptionsFromStorage(
storage,
deviceName,
{
...deviceOptionsFormData,
userDefineOptions: [...deviceOptionsResponse.targetDeviceOptions],
}
);
// if a network device is selected, merge in its options
if (selectedDevice && networkDevices.has(selectedDevice)) {
const networkDevice = networkDevices.get(selectedDevice);
userDefineOptions.userDefineOptions = userDefineOptions.userDefineOptions.map(
(userDefineOption) => {
const networkDeviceOption = networkDevice?.options.find(
(item) => item.key === userDefineOption.key
);
const newUserDefineOption = { ...userDefineOption };
if (networkDeviceOption) {
newUserDefineOption.enabled = networkDeviceOption.enabled;
newUserDefineOption.value = networkDeviceOption.value;
}
return newUserDefineOption;
}
);
}
setDeviceOptionsFormData(userDefineOptions);
};
const [
fetchOptions,
{
loading: loadingOptions,
data: deviceOptionsResponse,
error: deviceOptionsResponseError,
},
] = useTargetDeviceOptionsLazyQuery({
fetchPolicy: 'network-only',
onCompleted: (data) => {
handleDeviceOptionsResponse(data).catch((err) => {
console.error('failed to handle device options response', err);
});
},
});
useEffect(() => {
if (
deviceTarget === null ||
firmwareVersionData === null ||
validateFirmwareVersionData(firmwareVersionData).length > 0
) {
setDeviceOptionsFormData({
userDefinesTxt: '',
userDefinesMode: UserDefinesMode.UserInterface,
userDefineOptions: [],
});
} else {
fetchOptions({
variables: {
target: deviceTarget.name,
source: firmwareVersionData.source as FirmwareSource,
gitBranch: firmwareVersionData.gitBranch!,
gitTag: firmwareVersionData.gitTag!,
gitCommit: firmwareVersionData.gitCommit!,
localPath: firmwareVersionData.localPath!,
gitPullRequest: firmwareVersionData.gitPullRequest,
gitRepository: {
url: gitRepository.url,
owner: gitRepository.owner,
repositoryName: gitRepository.repositoryName,
rawRepoUrl: gitRepository.rawRepoUrl,
srcFolder: gitRepository.srcFolder,
},
},
});
}
}, [deviceTarget, firmwareVersionData, gitRepository, fetchOptions]);
const onResetToDefaults = () => {
const handleReset = async () => {
if (deviceOptionsResponse === undefined || deviceTarget === null) {
// eslint-disable-next-line no-alert
alert(`deviceOptionsResponse is undefined`);
return;
}
const deviceName = device?.name || null;
if (deviceName) {
const storage = new ApplicationStorage();
await storage.removeDeviceOptions(deviceName);
const userDefineOptions = await mergeWithDeviceOptionsFromStorage(
storage,
deviceName,
{
...deviceOptionsFormData,
userDefineOptions: [...deviceOptionsResponse.targetDeviceOptions],
}
);
setDeviceOptionsFormData(userDefineOptions);
}
};
handleReset().catch((err) => {
console.error(`failed to reset device options form data: ${err}`);
});
};
const onUserDefines = useCallback(
(data: DeviceOptionsFormData) => {
setDeviceOptionsFormData(data);
if (deviceTarget !== null) {
const storage = new ApplicationStorage();
const deviceName = device?.name;
if (deviceName) {
persistDeviceOptions(storage, deviceName, data).catch((err) => {
console.error(`failed to persist user defines: ${err}`);
});
}
}
},
[deviceTarget, deviceTargets]
);
const [
buildFlashFirmwareMutation,
{
loading: buildInProgress,
data: response,
error: buildFlashErrorResponse,
},
] = useBuildFlashFirmwareMutation();
useEffect(() => {
const arg = response?.buildFlashFirmware?.firmwareBinPath;
if (arg !== undefined && arg !== null && arg?.length > 0) {
const body: OpenFileLocationRequestBody = {
path: arg,
};
ipcRenderer.send(IpcRequest.OpenFileLocation, body);
}
}, [response]);
const isTX = useMemo(() => {
if (deviceTarget) {
return deviceTarget.name?.indexOf('_TX_') > -1;
}
return false;
}, [deviceTarget]);
const hasLuaScript = useMemo(() => {
return deviceType === DeviceType.ExpressLRS && isTX;
}, [deviceType, isTX]);
useEffect(() => {
if (firmwareVersionData && isTX && hasLuaScript) {
fetchLuaScript({
variables: {
source: firmwareVersionData.source as FirmwareSource,
gitBranch: firmwareVersionData.gitBranch!,
gitTag: firmwareVersionData.gitTag!,
gitCommit: firmwareVersionData.gitCommit!,
localPath: firmwareVersionData.localPath!,
gitPullRequest: firmwareVersionData.gitPullRequest,
gitRepository: {
url: gitRepository.url,
owner: gitRepository.owner,
repositoryName: gitRepository.repositoryName,
rawRepoUrl: gitRepository.rawRepoUrl,
srcFolder: gitRepository.srcFolder,
},
},
});
}
}, [gitRepository, firmwareVersionData, fetchLuaScript, isTX, hasLuaScript]);
/*
Display Electron.js confirmation dialog if user wants to shutdown the app
when build is in progress.
*/
useEffect(() => {
const body: UpdateBuildStatusRequestBody = {
buildInProgress,
};
ipcRenderer.send(IpcRequest.UpdateBuildStatus, body);
}, [buildInProgress]);
const [serialDevice, setSerialDevice] = useState<string | null>(null);
const onSerialDevice = (newSerialDevice: string | null) => {
setSerialDevice(newSerialDevice);
};
const [wifiDevice, setWifiDevice] = useState<string | null>(null);
const onWifiDevice = useCallback((newWifiDevice: string | null) => {
setWifiDevice(newWifiDevice);
}, []);
const [serialPortRequired, setSerialPortRequired] = useState<boolean>(false);
const [wifiDeviceRequired, setWifiDeviceRequired] = useState<boolean>(false);
useEffect(() => {
if (
deviceTarget &&
(deviceTarget.flashingMethod === FlashingMethod.BetaflightPassthrough ||
deviceTarget.flashingMethod === FlashingMethod.UART)
) {
setSerialPortRequired(true);
} else {
setSerialPortRequired(false);
}
if (deviceTarget && deviceTarget.flashingMethod === FlashingMethod.WIFI) {
setWifiDeviceRequired(true);
} else {
setWifiDeviceRequired(false);
}
}, [deviceTarget, deviceTarget, deviceTargets]);
const [
deviceOptionsValidationErrors,
setDeviceOptionsValidationErrors,
] = useState<Error[] | null>(null);
const reset = () => {
logsRef.current = [];
progressNotificationsRef.current = [];
setLogs('');
setFirmwareVersionErrors([]);
setDeviceTargetErrors([]);
setDeviceOptionsValidationErrors([]);
setProgressNotifications([]);
setLastProgressNotification(null);
};
const onBack = () => {
reset();
setViewState(ViewState.Configuration);
setAppStatus(AppStatus.Interactive);
};
const getAbbreviatedDeviceName = (item: Device) => {
return item.abbreviatedName?.slice(0, 16) ?? item.name?.slice(0, 16);
};
const [currentJobType, setCurrentJobType] = useState<BuildJobType>(
BuildJobType.Build
);
const sendJob = (type: BuildJobType) => {
reset();
setCurrentJobType(type);
// Validate firmware source
if (firmwareVersionData === null) {
setFirmwareVersionErrors([new Error('Please select firmware source')]);
return;
}
const sourceErrors = validateFirmwareVersionData(firmwareVersionData);
if (sourceErrors.length > 0) {
setFirmwareVersionErrors(sourceErrors);
return;
}
// Validate device target
if (deviceTarget === null) {
setDeviceTargetErrors([new Error('Please select a device target')]);
return;
}
// Validate device options
if (deviceOptionsFormData === null) {
setDeviceTargetErrors([
new Error('Please configure your device options'),
]);
return;
}
switch (deviceOptionsFormData.userDefinesMode) {
case UserDefinesMode.Manual:
break;
case UserDefinesMode.UserInterface:
const errs = new UserDefinesValidator().validate(
deviceOptionsFormData.userDefineOptions
);
if (errs.length > 0) {
setDeviceOptionsValidationErrors(errs);
return;
}
break;
default:
break;
}
let uploadPort: string | undefined;
if (serialPortRequired && serialDevice != null) {
uploadPort = serialDevice;
} else if (wifiDeviceRequired && wifiDevice !== null) {
uploadPort = wifiDevice;
}
const userDefines = deviceOptionsFormData.userDefineOptions.map((item) => ({
key: item.key,
value: item.value,
enabled: item.enabled,
enumValues: item.enumValues,
type: item.type,
}));
if (device?.parent && device?.name) {
const deviceName = getAbbreviatedDeviceName(device);
// add the user define for the device name
userDefines.push({
key: UserDefineKey.DEVICE_NAME,
value: deviceName,
enabled: true,
enumValues: null,
type: UserDefineKind.Text,
});
}
const input: BuildFlashFirmwareInput = {
type,
firmware: firmwareVersionData,
target: deviceTarget.name,
userDefinesTxt: deviceOptionsFormData.userDefinesTxt,
userDefinesMode: deviceOptionsFormData.userDefinesMode,
userDefines,
serialDevice: uploadPort,
};
buildFlashFirmwareMutation({
variables: {
input,
gitRepository: {
url: gitRepository.url,
owner: gitRepository.owner,
repositoryName: gitRepository.repositoryName,
rawRepoUrl: gitRepository.rawRepoUrl,
srcFolder: gitRepository.srcFolder,
},
},
});
setViewState(ViewState.Compiling);
setAppStatus(AppStatus.Busy);
};
useEffect(() => {
if (
!buildInProgress &&
response?.buildFlashFirmware?.success !== undefined
) {
window.scrollTo(0, document.body.scrollHeight);
}
}, [buildInProgress, response]);
const onBuild = () => sendJob(BuildJobType.Build);
const onBuildAndFlash = () => sendJob(BuildJobType.BuildAndFlash);
const onForceFlash = () => sendJob(BuildJobType.ForceFlash);
const deviceTargetRef = useRef<HTMLDivElement | null>(null);
const deviceOptionsRef = useRef<HTMLDivElement | null>(null);
const [
deviceSelectErrorDialogOpen,
setDeviceSelectErrorDialogOpen,
] = useState<boolean>(false);
const handleSelectedDeviceChange = useCallback(
(deviceName: string) => {
const dnsDevice = networkDevices.get(deviceName);
if (dnsDevice) {
const dnsDeviceName = dnsDevice.deviceName?.toUpperCase();
const dnsDeviceTarget = dnsDevice.target.toUpperCase();
let deviceMatches: Device[] | undefined = [];
// try to find the device by the deviceName
deviceMatches = deviceTargets?.filter((item) => {
return getAbbreviatedDeviceName(item).toUpperCase() === dnsDeviceName;
});
// if no matches found by deviceName, then use the target
if (
deviceMatches?.length === 0 &&
dnsDeviceTarget.trim().length !== 0
) {
deviceMatches = deviceTargets?.filter((item) => {
// only match on a device that doesn't have a parent, which means it
// is not an alias of another device
return (
!item.parent &&
item.targets.find((target) => {
const baseTargetName = target.name.split('_via_')[0];
return baseTargetName.toUpperCase() === dnsDeviceTarget;
})
);
});
}
// if no device is found that matches the target
if (!deviceMatches || deviceMatches.length === 0) {
console.error(
`no device matches found for target ${dnsDeviceTarget}!`
);
setDeviceSelectErrorDialogOpen(true);
return;
}
// if multiple device matches are found, then don't select any of them
// we do not know which one is correct and do not want to pick the wrong device.
if (deviceMatches.length > 1) {
console.error(
`multiple device matches found for target ${dnsDeviceTarget}!`
);
setDeviceSelectErrorDialogOpen(true);
return;
}
const deviceMatch = deviceMatches[0];
const dTarget =
deviceMatch?.targets.find((target) => {
return target.flashingMethod === FlashingMethod.WIFI;
}) ||
deviceMatch?.targets[0] ||
null;
if (dTarget !== deviceTarget) {
setDeviceTarget(dTarget);
deviceTargetRef?.current?.scrollIntoView({ behavior: 'smooth' });
}
setWifiDevice(dnsDevice.ip);
}
},
[deviceTarget, deviceTargets, networkDevices]
);
useEffect(() => {
if (selectedDevice) {
handleSelectedDeviceChange(selectedDevice);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDevice]);
const luaDownloadButton = () => {
if (
hasLuaScript &&
luaScriptResponse &&
luaScriptResponse.luaScript.fileLocation &&
luaScriptResponse.luaScript.fileLocation.length > 0
) {
return (
<Button
sx={styles.button}
color="primary"
size="large"
variant="contained"
href={luaScriptResponse?.luaScript.fileLocation ?? ''}
download
>
Download LUA script
</Button>
);
}
return null;
};
const handleDeviceSelectErrorDialogClose = useCallback(() => {
setDeviceSelectErrorDialogOpen(false);
}, []);
const saveBuildLogToFile = useCallback(async () => {
const saveFileRequestBody: SaveFileRequestBody = {
data: logs,
defaultPath: `ExpressLRSBuildLog_${new Date()
.toISOString()
.replace(/[^0-9]/gi, '')}.txt`,
};
const result: SaveFileResponseBody = await ipcRenderer.invoke(
IpcRequest.SaveFile,
saveFileRequestBody
);
if (result.success) {
const openFileLocationRequestBody: OpenFileLocationRequestBody = {
path: result.path,
};
ipcRenderer.send(
IpcRequest.OpenFileLocation,
openFileLocationRequestBody
);
}
}, [logs]);
return (
<MainLayout>
{viewState === ViewState.Configuration && (
<>
<Card>
<CardTitle icon={<SettingsIcon />} title="Firmware version" />
<Divider />
<CardContent>
<FirmwareVersionForm
onChange={onFirmwareVersionData}
data={firmwareVersionData}
gitRepository={gitRepository}
/>
<ShowAlerts severity="error" messages={firmwareVersionErrors} />
</CardContent>
<Divider />
<CardTitle icon={<SettingsIcon />} title="Target" />
<Divider />
<CardContent ref={deviceTargetRef}>
{firmwareVersionData === null ||
(validateFirmwareVersionData(firmwareVersionData).length >
0 && (
<Alert severity="info">
<AlertTitle>Notice</AlertTitle>
Please select a firmware version first
</Alert>
))}
{!loadingTargets && !targetsResponseError && (
<DeviceTargetForm
currentTarget={deviceTarget}
onChange={onDeviceTarget}
firmwareVersionData={firmwareVersionData}
deviceOptions={deviceTargets}
/>
)}
<Loader loading={loadingTargets} />
{luaDownloadButton()}
{hasLuaScript && (
<ShowAlerts
severity="error"
messages={luaScriptResponseError}
/>
)}
<ShowAlerts severity="error" messages={targetsResponseError} />
<ShowAlerts severity="error" messages={deviceTargetErrors} />
</CardContent>
<Divider />
<CardTitle
icon={<SettingsIcon />}
title={
<div ref={deviceOptionsRef}>
Device options{' '}
{deviceOptionsFormData.userDefinesMode ===
UserDefinesMode.UserInterface &&
deviceTarget !== null &&
!loadingOptions && (
<Tooltip
placement="top"
arrow
title={
<div>
Reset device options to the recommended defaults on
this device target. Except for your custom binding
phrase.
</div>
}
>
<Button onClick={onResetToDefaults} size="small">
Reset
</Button>
</Tooltip>
)}
</div>
}
/>
<Divider />
<CardContent>
{!loadingOptions && (
<DeviceOptionsForm
target={deviceTarget?.name ?? null}
deviceOptions={deviceOptionsFormData}
firmwareVersionData={firmwareVersionData}
onChange={onUserDefines}
/>
)}
{deviceOptionsFormData.userDefinesMode ===
UserDefinesMode.UserInterface &&
(firmwareVersionData === null ||
validateFirmwareVersionData(firmwareVersionData).length > 0 ||
deviceTarget === null) && (
<Alert severity="info">
<AlertTitle>Notice</AlertTitle>
Please select a firmware version and device target first
</Alert>
)}
<ShowAlerts
severity="error"
messages={deviceOptionsResponseError}
/>
<ShowAlerts
severity="error"
messages={deviceOptionsValidationErrors}
/>
<Loader loading={loadingOptions} />
</CardContent>
<Divider />
<CardTitle icon={<SettingsIcon />} title="Actions" />
<Divider />
<CardContent>
<UserDefinesAdvisor
deviceOptionsFormData={deviceOptionsFormData}
/>
<div>
{serialPortRequired && (
<SerialDeviceSelect
serialDevice={serialDevice}
onChange={onSerialDevice}
/>
)}
{wifiDeviceRequired && (
<WifiDeviceSelect
wifiDevice={wifiDevice}
wifiDevices={Array.from(networkDevices.values()).filter(
(item) => {
return deviceTarget?.name
?.toUpperCase()
.startsWith(item.target.toUpperCase());
}
)}
onChange={onWifiDevice}
/>
)}
<Button
sx={styles.button}
size="large"
variant="contained"
onClick={onBuild}
>
Build
</Button>
{deviceTarget?.flashingMethod !== FlashingMethod.Radio && (
<SplitButton
sx={styles.button}
size="large"
variant="contained"
options={[
{
label: 'Build & Flash',
value: BuildJobType.BuildAndFlash,
},
{
label: 'Force Flash',
value: BuildJobType.ForceFlash,
},
]}
onButtonClick={(value: string | null) => {
if (value === BuildJobType.BuildAndFlash) {
onBuildAndFlash();
} else if (value === BuildJobType.ForceFlash) {
onForceFlash();
}
}}
/>
)}
</div>
</CardContent>
</Card>
<Card>
{networkDevices.size > 0 && (
<Box>
<Divider />
<CardTitle icon={<NetworkWifi />} title="Network Devices" />
<Divider />
<CardContent>
<div>
<WifiDeviceList
wifiDevices={Array.from(networkDevices.values())}
onChange={(dnsDevice: MulticastDnsInformation) => {
onDeviceChange(dnsDevice);
handleSelectedDeviceChange(dnsDevice.name);
}}
/>
</div>
</CardContent>
</Box>
)}
</Card>
<Dialog
open={deviceSelectErrorDialogOpen}
onClose={handleDeviceSelectErrorDialogClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Device Select Error
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
The target device could not be automatically selected, it must
be done manually.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleDeviceSelectErrorDialogClose}>
Close
</Button>
</DialogActions>
</Dialog>
</>
)}
{viewState === ViewState.Compiling && (
<Card>
<CardTitle icon={<SettingsIcon />} title="Build" />
<Divider />
<CardContent>
<BuildProgressBar
inProgress={buildInProgress}
jobType={currentJobType}
progressNotification={lastProgressNotification}
/>
<BuildNotificationsList notifications={progressNotifications} />
<ShowAlerts severity="error" messages={buildFlashErrorResponse} />
</CardContent>
{logs.length > 0 && (
<>
<CardTitle
icon={<SettingsIcon />}
title={
<Box display="flex" justifyContent="space-between">
<Box>Logs</Box>
<Box>
<IconButton
aria-label="Copy log to clipboard"
title="Copy log to clipboard"
onClick={async () => {
await navigator.clipboard.writeText(logs);
}}
>
<ContentCopy />
</IconButton>
<IconButton
aria-label="Save log to file"
title="Save log to file"
onClick={saveBuildLogToFile}
>
<Save />
</IconButton>
</Box>
</Box>
}
/>
<Divider />
<CardContent>
<Box sx={styles.longBuildDurationWarning}>
<ShowTimeoutAlerts
severity="warning"
messages="Sometimes builds take at least a few minutes. It is normal, especially for the first time builds."
active={buildInProgress}
timeout={14 * 1000}
/>
</Box>
<Logs data={logs} />
</CardContent>
<Divider />
</>
)}
{response !== undefined && (
<>
<CardTitle icon={<SettingsIcon />} title="Result" />
<Divider />
<CardContent>
{response?.buildFlashFirmware?.success &&
currentJobType === BuildJobType.BuildAndFlash &&
deviceTarget?.flashingMethod === FlashingMethod.WIFI && (
<>
<Alert sx={styles.buildNotification} severity="warning">
<AlertTitle>Warning</AlertTitle>
Please wait for LED to resume blinking before
disconnecting power
</Alert>
</>
)}
<ShowAfterTimeout
timeout={
response?.buildFlashFirmware?.success &&
currentJobType === BuildJobType.BuildAndFlash &&
deviceTarget?.flashingMethod === FlashingMethod.WIFI
? 15000
: 1000
}
active={!buildInProgress}
>
<Box sx={styles.buildNotification}>
<BuildResponse
response={response?.buildFlashFirmware}
firmwareVersionData={firmwareVersionData}
/>
</Box>
{response?.buildFlashFirmware?.success && hasLuaScript && (
<>
<Alert sx={styles.buildNotification} severity="info">
<AlertTitle>Update Lua Script</AlertTitle>
Make sure to update the Lua script on your radio
</Alert>
</>
)}
</ShowAfterTimeout>
{response?.buildFlashFirmware?.success &&
currentJobType === BuildJobType.Build && (
<>
<Alert sx={styles.buildNotification} severity="info">
<AlertTitle>Build notice</AlertTitle>
{deviceTarget?.flashingMethod !== FlashingMethod.Radio
? 'Firmware binary file was opened in the file explorer'
: "Firmware binary file was opened in the file explorer, copy the firmware file to your radios's SD card and flash it to the transmitter using EdgeTX/OpenTX"}
</Alert>
</>
)}
</CardContent>
<Divider />
</>
)}
{!buildInProgress && (
<>
<CardTitle icon={<SettingsIcon />} title="Actions" />
<Divider />
<CardContent>
<Button
sx={styles.button}
color="primary"
size="large"
variant="contained"
onClick={onBack}
>
Back
</Button>
{!response?.buildFlashFirmware.success && (
<Button
sx={styles.button}
size="large"
variant="contained"
onClick={() => {
sendJob(currentJobType);
}}
>
Retry
</Button>
)}
{!response?.buildFlashFirmware.success &&
response?.buildFlashFirmware.errorType ===
BuildFirmwareErrorType.TargetMismatch && (
<Button
sx={styles.button}
size="large"
variant="contained"
onClick={onForceFlash}
>
Force Flash
</Button>
)}
{response?.buildFlashFirmware.success && luaDownloadButton()}
</CardContent>
</>
)}
</Card>
)}
</MainLayout>
);
}
Example #17
Source File: index.tsx From ExpressLRS-Configurator with GNU General Public License v3.0 | 4 votes |
FirmwareVersionForm: FunctionComponent<FirmwareVersionCardProps> = (
props
) => {
const { onChange, data, gitRepository } = props;
const [firmwareSource, setFirmwareSource] = useState<FirmwareSource>(
data?.source || FirmwareSource.GitTag
);
const handleFirmwareSourceChange = (
_event: React.SyntheticEvent,
value: FirmwareSource
) => {
setFirmwareSource(value);
};
const [showPreReleases, setShowPreReleases] = useState<boolean>(false);
useEffect(() => {
new ApplicationStorage()
.getShowPreReleases(false)
.then((value) => {
setShowPreReleases(value);
})
.catch((err: Error) => {
console.error('failed to get show pre-releases from storage', err);
});
}, []);
const onShowPreReleases = (
_event: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
setShowPreReleases(checked);
new ApplicationStorage().setShowPreReleases(checked).catch((err: Error) => {
console.error('failed to set show pre-releases in storage', err);
});
};
const [
queryGitTags,
{ loading: gitTagsLoading, data: gitTagsResponse, error: tagsError },
] = useGetReleasesLazyQuery();
const [
queryGitBranches,
{
loading: gitBranchesLoading,
data: gitBranchesResponse,
error: branchesError,
},
] = useGetBranchesLazyQuery();
const [
queryGitPullRequests,
{
loading: gitPullRequestsLoading,
data: gitPullRequestsResponse,
error: pullRequestsError,
},
] = useGetPullRequestsLazyQuery();
const loading =
gitTagsLoading || gitBranchesLoading || gitPullRequestsLoading;
const gitTags = useMemo(() => {
return (
gitTagsResponse?.releases.filter(
({ tagName }) => !gitRepository.tagExcludes.includes(tagName)
) ?? []
).sort((a, b) => semver.rcompare(a.tagName, b.tagName));
}, [gitRepository.tagExcludes, gitTagsResponse?.releases]);
const gitBranches = useMemo(() => {
return gitBranchesResponse?.gitBranches ?? [];
}, [gitBranchesResponse?.gitBranches]);
const gitPullRequests = gitPullRequestsResponse?.pullRequests;
const [currentGitTag, setCurrentGitTag] = useState<string>(
data?.gitTag || ''
);
const onGitTag = (name: string | null) => {
if (name === null) {
setCurrentGitTag('');
return;
}
setCurrentGitTag(name);
};
/*
We need to make sure that a valid value is selected
*/
useEffect(() => {
if (firmwareSource === FirmwareSource.GitTag) {
if (
!showPreReleases &&
gitTags?.length &&
gitTags?.length > 0 &&
gitTags
?.filter(({ preRelease }) => !preRelease)
.find((item) => item.tagName === currentGitTag) === undefined
) {
setCurrentGitTag(gitTags[0].tagName);
}
}
}, [showPreReleases, currentGitTag, gitTags, firmwareSource]);
const [currentGitBranch, setCurrentGitBranch] = useState<string>(
data?.gitBranch || ''
);
const onGitBranch = (name: string | null) => {
if (name === null) {
setCurrentGitBranch('');
return;
}
setCurrentGitBranch(name);
};
const [currentGitCommit, setCurrentGitCommit] = useState<string>(
data?.gitCommit || ''
);
const [debouncedGitCommit, setDebouncedGitCommit] = useState<string>(
data?.gitCommit || ''
);
const debouncedGitCommitHandler = useMemo(
() => debounce(setDebouncedGitCommit, 1000),
[setDebouncedGitCommit]
);
// Stop the invocation of the debounced function
// after unmounting
useEffect(() => {
return () => {
debouncedGitCommitHandler.cancel();
};
}, [debouncedGitCommitHandler]);
const setGitCommit = (value: string) => {
setCurrentGitCommit(value);
debouncedGitCommitHandler(value);
};
const onGitCommit = (event: React.ChangeEvent<HTMLInputElement>) => {
setGitCommit(event.target.value);
};
const [localPath, setLocalPath] = useState<string>(data?.localPath || '');
const onLocalPath = (event: React.ChangeEvent<HTMLInputElement>) => {
setLocalPath(event.target.value);
};
const [
currentGitPullRequest,
setCurrentGitPullRequest,
] = useState<PullRequestInput | null>(data?.gitPullRequest || null);
/*
Make sure that a valid pull request is selected
*/
useEffect(() => {
if (gitPullRequestsResponse?.pullRequests && currentGitPullRequest) {
const pullRequest =
gitPullRequestsResponse.pullRequests.find(
(item) => item.number === currentGitPullRequest.number
) || null;
// if we have a list of pull requests and the current pull request is not
// part of that list, then set current pull request to null
if (!pullRequest) {
setCurrentGitPullRequest(null);
}
// prevent stale head commit hash cache
if (
pullRequest &&
pullRequest.headCommitHash !== currentGitPullRequest.headCommitHash
) {
setCurrentGitPullRequest({
id: pullRequest.id,
number: pullRequest.number,
title: pullRequest.title,
headCommitHash: pullRequest.headCommitHash,
});
}
}
}, [gitPullRequestsResponse, currentGitPullRequest]);
const onGitPullRequest = (value: string | null) => {
if (value === null) {
setCurrentGitPullRequest(null);
return;
}
const iValue = parseInt(value, 10);
const pullRequest = gitPullRequests?.find((item) => item.number === iValue);
if (pullRequest) {
setCurrentGitPullRequest({
id: pullRequest.id,
number: pullRequest.number,
title: pullRequest.title,
headCommitHash: pullRequest.headCommitHash,
});
}
};
useEffect(() => {
const storage = new ApplicationStorage();
storage
.getFirmwareSource(gitRepository)
.then((result) => {
if (result !== null) {
if (result.source) setFirmwareSource(result.source);
if (result.gitTag) setCurrentGitTag(result.gitTag);
if (result.gitCommit) setGitCommit(result.gitCommit);
if (result.gitBranch) setCurrentGitBranch(result.gitBranch);
if (result.localPath) setLocalPath(result.localPath);
if (result.gitPullRequest)
setCurrentGitPullRequest(result.gitPullRequest);
}
})
.catch((err) => {
console.error('failed to get firmware source', err);
});
}, []);
const onChooseFolder = () => {
ipcRenderer
.invoke(IpcRequest.ChooseFolder)
.then((result: ChooseFolderResponseBody) => {
if (result.success) {
setLocalPath(result.directoryPath);
}
})
.catch((err) => {
console.error('failed to get local directory path: ', err);
});
};
useEffect(() => {
switch (firmwareSource) {
case FirmwareSource.GitTag:
queryGitTags({
variables: {
owner: gitRepository.owner,
repository: gitRepository.repositoryName,
},
});
break;
case FirmwareSource.GitBranch:
queryGitBranches({
variables: {
owner: gitRepository.owner,
repository: gitRepository.repositoryName,
},
});
break;
case FirmwareSource.GitCommit:
break;
case FirmwareSource.Local:
break;
case FirmwareSource.GitPullRequest:
queryGitPullRequests({
variables: {
owner: gitRepository.owner,
repository: gitRepository.repositoryName,
},
});
break;
default:
throw new Error(`unknown firmware source: ${firmwareSource}`);
}
}, [
gitRepository,
firmwareSource,
queryGitTags,
queryGitBranches,
queryGitPullRequests,
]);
useEffect(() => {
const updatedData = {
source: firmwareSource,
gitBranch: currentGitBranch,
gitTag: currentGitTag,
gitCommit: debouncedGitCommit,
localPath,
gitPullRequest: currentGitPullRequest,
};
onChange(updatedData);
const storage = new ApplicationStorage();
storage.setFirmwareSource(updatedData, gitRepository).catch((err) => {
console.error('failed to set firmware source', err);
});
}, [
firmwareSource,
currentGitBranch,
currentGitTag,
debouncedGitCommit,
localPath,
currentGitPullRequest,
onChange,
gitRepository,
]);
const gitTagOptions = useMemo(() => {
return gitTags
.filter((item) => {
if (!showPreReleases) {
return item.preRelease === false;
}
return true;
})
.map((item) => ({
label: item.tagName,
value: item.tagName,
}));
}, [gitTags, showPreReleases]);
const gitBranchOptions = useMemo(() => {
return gitBranches.map((branch) => ({
label: branch,
value: branch,
}));
}, [gitBranches]);
const gitPullRequestOptions = useMemo(() => {
return gitPullRequests?.map((pullRequest) => ({
label: `${pullRequest.title} #${pullRequest.number}`,
value: `${pullRequest.number}`,
}));
}, [gitPullRequests]);
const showBetaFpvAlert =
localPath?.toLocaleLowerCase()?.indexOf('betafpv') > -1;
return (
<>
<Tabs
sx={styles.tabs}
defaultValue={FirmwareSource.GitTag}
value={firmwareSource}
onChange={handleFirmwareSourceChange}
>
<Tab label="Official releases" value={FirmwareSource.GitTag} />
<Tab label="Git branch" value={FirmwareSource.GitBranch} />
<Tab label="Git commit" value={FirmwareSource.GitCommit} />
<Tab label="Local" value={FirmwareSource.Local} />
<Tab label="Git Pull Request" value={FirmwareSource.GitPullRequest} />
</Tabs>
{firmwareSource === FirmwareSource.GitTag && gitTags !== undefined && (
<>
<Box sx={styles.tabContents}>
{!loading && (
<>
<FormControlLabel
sx={styles.preReleaseCheckbox}
control={
<Checkbox
checked={showPreReleases}
onChange={onShowPreReleases}
/>
}
label="Show pre-releases"
/>
<Omnibox
title="Releases"
options={gitTagOptions}
currentValue={
gitTagOptions.find(
(item) => item.value === currentGitTag
) ?? null
}
onChange={onGitTag}
/>
<Button
size="small"
sx={styles.releaseNotes}
target="_blank"
rel="noreferrer noreferrer"
href={`${gitRepository.url}/releases/tag/${currentGitTag}`}
>
Release notes
</Button>
{currentGitTag &&
gitTagOptions.length > 0 &&
gitTagOptions[0]?.value !== currentGitTag && (
<Alert sx={styles.firmwareVersionAlert} severity="info">
There is a newer version of the firmware available
</Alert>
)}
</>
)}
</Box>
</>
)}
{firmwareSource === FirmwareSource.GitBranch &&
gitBranches !== undefined && (
<>
<Alert severity="warning" sx={styles.dangerZone}>
<AlertTitle>DANGER ZONE</AlertTitle>
Use these sources only if you know what you are doing or was
instructed by project developers
</Alert>
<Box sx={styles.tabContents}>
{!loading && (
<Omnibox
title="Git branches"
options={gitBranchOptions}
currentValue={
gitBranchOptions.find(
(item) => item.value === currentGitBranch
) ?? null
}
onChange={onGitBranch}
/>
)}
</Box>
</>
)}
{firmwareSource === FirmwareSource.GitCommit && (
<>
<Alert severity="warning" sx={styles.dangerZone}>
<AlertTitle>DANGER ZONE</AlertTitle>
Use these sources only if you know what you are doing or was
instructed by project developers
</Alert>
<Box sx={styles.tabContents}>
<TextField
id="git-commit-hash"
label="Git commit hash"
fullWidth
value={currentGitCommit}
onChange={onGitCommit}
/>
</Box>
</>
)}
{firmwareSource === FirmwareSource.Local && (
<>
<Alert severity="warning" sx={styles.dangerZone}>
<AlertTitle>DANGER ZONE</AlertTitle>
Use these sources only if you know what you are doing or was
instructed by project developers
</Alert>
<Box sx={styles.tabContents}>
<TextField
id="local-path"
label="Local path"
fullWidth
value={localPath}
onChange={onLocalPath}
/>
{showBetaFpvAlert && (
<Alert severity="error" sx={styles.betaFpvAlert}>
<AlertTitle>ATTENTION</AlertTitle>
You are trying to flash an outdated BetaFPV custom ExpressLRS
fork. BetaFPV hardware is fully supported in recent official
ExpressLRS releases. We recommend using official firmware to
have the best ExpressLRS experience.
</Alert>
)}
<Button
color="secondary"
size="small"
variant="contained"
sx={styles.chooseFolderButton}
onClick={onChooseFolder}
>
Choose folder
</Button>
</Box>
</>
)}
{firmwareSource === FirmwareSource.GitPullRequest &&
gitPullRequests !== undefined && (
<>
<Alert severity="warning" sx={styles.dangerZone}>
<AlertTitle>DANGER ZONE</AlertTitle>
Use these sources only if you know what you are doing or was
instructed by project developers
</Alert>
<Box sx={styles.tabContents}>
{!loading && (
<Omnibox
title="Git pull Requests"
options={gitPullRequestOptions ?? []}
currentValue={
gitPullRequestOptions?.find(
(item) =>
item.value === `${currentGitPullRequest?.number}`
) ?? null
}
onChange={onGitPullRequest}
/>
)}
</Box>
</>
)}
<Loader loading={loading} />
<ShowAlerts severity="error" messages={branchesError} />
<ShowAlerts severity="error" messages={tagsError} />
<ShowAlerts severity="error" messages={pullRequestsError} />
</>
);
}
Example #18
Source File: index.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
Release: FC = () => {
const [update, setUpdate] = React.useState(false);
const [remind, setRemind] =
React.useState<AccountUpdateMessageRemind>('popup');
const [interval, setInterval] = React.useState(0);
const [messageData, setMessageData] = React.useState({} as AuthMessage);
const init = () => {
const account = localStorage.getItem('account');
const result = getAuthDataByKey(account ?? '', 'message');
if (isBoolean(result?.update)) {
setUpdate(result.update);
setRemind('popup');
setInterval(0);
} else {
const { update = {} } = result || {};
const {
update: privUpdate = true,
remind = 'popup',
interval = 0,
} = update;
setUpdate(privUpdate);
setRemind(remind);
setInterval(interval);
}
setMessageData(result);
};
const handleUpdate = (key: any, val: any) => {
const account = localStorage.getItem('account');
const updateData: any = {
update,
interval,
remind,
lastTime: dayjs(),
};
updateData[key] = val;
const newMessageData = {
...messageData,
update: updateData,
};
setMessageData(newMessageData);
updateAuthDataByKey(account ?? '', 'message', newMessageData);
init();
};
const onUpdateSwichChange = (
_: React.ChangeEvent<HTMLInputElement>,
checked: boolean,
) => {
setUpdate(checked);
handleUpdate('update', checked);
};
const onRemindChange = (e: SelectChangeEvent<any>) => {
const value = e.target.value as AccountUpdateMessageRemind;
setRemind(value);
handleUpdate('remind', value);
};
const onIntervalChange = (e: SelectChangeEvent<any>) => {
const value = e.target.value as number;
setInterval(value);
handleUpdate('interval', value);
};
useEffect(() => {
init();
}, []);
return (
<div>
<ContentList>
<Alert severity="info">
<AlertTitle>提示</AlertTitle>
修改任意配置都会重置版本更新时间间隔依赖的时间
</Alert>
<ItemCard
title="版本更新提醒"
desc="设置版本更新时是否提醒"
action={<Switch checked={update} onChange={onUpdateSwichChange} />}
/>
<ItemCard
title="提醒方式"
desc="设置版本更新提醒方式"
action={
<Select
size="small"
label="提醒方式"
value={remind}
options={[
{ label: '消息', value: 'message' },
// { label: '通知', value: 'notification' },
{ label: '弹窗', value: 'popup' },
]}
onChange={onRemindChange}
/>
}
/>
<ItemCard
title="提醒间隔"
desc="设置版本更新提醒时间间隔"
action={
<Select
size="small"
label="提醒间隔"
value={interval}
options={[
{ label: '随时', value: 0 },
{ label: '7天', value: 7 },
{ label: '30天', value: 30 },
{ label: '60天', value: 60 },
{ label: '90天', value: 90 },
]}
onChange={onIntervalChange}
/>
}
/>
</ContentList>
</div>
);
}
Example #19
Source File: index.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
Weather: FC = () => {
let timer: number | undefined; // 定时器(点击授权时检查是否授权)
let lastState: string | undefined; // 上一次的状态(记录单次点击授权最后一次状态,undefined时表示第一次点击)
const userId = localStorage.getItem('account') ?? '';
const [weather, setWeather] = React.useState<QWeatherNow>({} as QWeatherNow);
const [location, setLocation] = React.useState<QWeatherCity>(
{} as QWeatherCity,
);
const [permission, setPermission] = React.useState<boolean>(false);
const [status, setStatus] = React.useState<string>('');
const [geolocationStatus, setGeolocationStatusStatus] =
React.useState<boolean>(false);
const [key, setKey] = useState('');
const [pluginKey, setPluginKey] = useState('');
const [loading, setLoading] = useState(false);
const [latlng, setLatlng] = useState<number[]>([]);
const [weatherInterval, setWeatherInterval] = useState(15);
const [show, setShow] = useState(true);
const refreshOptions = [
{ label: '10分钟', value: 10 },
{ label: '15分钟', value: 15 },
{ label: '30分钟', value: 30 },
];
// 获取当前位置并获取天气
const getCurrentPosition = () => {
geolocation.getCurrentPosition().then((res) => {
const localData = getWeather(userId);
const time = dayjs(localData?.updatedTime ?? localData?.createdTime);
const diff = localData ? dayjs().diff(time, 'minute') > 10 : true;
setKey(localData?.key ?? '');
setPluginKey(localData?.pluginKey ?? '');
if (diff) {
setLatlng([res.longitude, res.latitude]);
getLocationInfo({
key: key ?? localData?.key,
location: res.longitude + ',' + res.latitude,
});
getWeatherInfo({
key: key ?? localData?.key,
location: res.longitude + ',' + res.latitude,
});
} else if (localData) {
localData.weather && setWeather(localData.weather);
localData.city && setLocation(localData.city);
}
});
};
const applyPermission = () => {
if (geolocation.checkGeolocation) {
/* 地理位置服务可用 */
setPermission(true);
geolocation.getPermissionStatus().then((res) => {
if (res === 'granted') {
setGeolocationStatusStatus(true);
getCurrentPosition();
} else {
setGeolocationStatusStatus(false);
}
});
} else {
/* 地理位置服务不可用 */
setPermission(false);
}
};
// 检查授权状态
const checkPermission = () => {
getCurrentPosition();
timer = setInterval(async () => {
geolocation.getPermissionStatus().then((res) => {
setGeolocationStatusStatus(res === 'granted');
setStatus(res);
if (res !== 'prompt') {
clearTimeout(timer);
!lastState && toast.info('已选择位置信息权限,请检查浏览器设置');
return;
}
lastState = res;
});
}, 100);
};
// 获取位置城市信息
const getLocationInfo = (params: QweatherCityParams) => {
setLoading(true);
const { key } = params;
locationInfo(params).then((res) => {
setLocation(key ? res : res.data);
setLoading(false);
});
};
// 获取天气信息
const getWeatherInfo = (params: QweatherNowParams) => {
setLoading(true);
const { key } = params;
qweatherNow(params).then((res) => {
setWeather(key ? res : res.data);
setLoading(false);
});
};
// 获取主页 天气设置
const getWeatherSetting = () => {
const res = getIndexWeatherSetting(userId);
const setting = res?.navBar?.left?.weather;
if (setting) {
setWeatherInterval(setting.interval);
setShow(setting.show);
}
};
useEffect(() => {
applyPermission();
getWeatherSetting();
}, []);
useEffect(() => {
// 保存天气信息前校验是否超过十分钟,填写key时不校验
const localData = getWeather(userId);
const time = dayjs(localData?.updatedTime ?? localData?.createdTime);
const diff = localData ? dayjs().diff(time, 'minute') > 10 : true;
if (
Object.keys(weather).length > 0 &&
Object.keys(location).length > 0 &&
(diff || !!localData?.key)
) {
saveWeather({
userId,
weather: weather,
city: location,
key: key,
latlng,
});
}
}, [weather, location]);
useEffect(() => {
saveIndexWeatherSetting({
userId,
interval: weatherInterval,
show,
});
}, [weatherInterval, show]);
return (
<div>
{geolocationStatus && (
<WeatherCard
apiKey={key}
onRefresh={() => getCurrentPosition()}
weather={weather}
city={location}
loading={loading}
/>
)}
<ContentList>
<ContentTitle title="权限"></ContentTitle>
<ItemAccordion
title="位置访问"
desc="获取用户地理位置信息,用于天气查询"
action={
<Switch
disabled={!permission}
onClick={(e) => e.stopPropagation()}
checked={geolocationStatus}
onChange={(e) => {
checkPermission();
}}
/>
}
>
{!permission && (
<Alert severity="warning">当前浏览器位置访问权限不可用</Alert>
)}
{status === 'granted' && (
<Alert severity="success">已授权位置访问权限</Alert>
)}
{status === 'denied' && (
<Alert severity="error">位置访问权限被拒绝,请检查浏览器设置</Alert>
)}
{status === 'prompt' && (
<Alert severity="info">等待授权位置访问权限</Alert>
)}
</ItemAccordion>
<ContentTitle title="KEY"></ContentTitle>
<Alert severity="info">
<AlertTitle>为什么需要填写KEY?</AlertTitle>
虽然和风天气提供了免费方案,但考虑到使用次数限制,最好的方式是自己申请KEY,然后填写到下方。
当然不填写KEY也可以使用天气功能,但是查询次数会有限制,如果超过限制,则无法使用天气功能。
</Alert>
<ItemAccordion title="和风天气KEY" desc="设置和风天气使用时必须的KEY">
<Alert
severity="warning"
className={css`
margin-bottom: 8px;
`}
>
该KEY仅用作和风天气API使用,不会保存到服务器,请勿将KEY泄露给他人。
</Alert>
<TextField
fullWidth
variant="standard"
label="和风天气API KEY"
placeholder="请输入和风天气API KEY"
value={key}
disabled={!permission}
onChange={(e) => {
setKey(e.target.value);
}}
onBlur={() => {
saveWeather({
userId,
weather: weather,
city: location,
key,
});
}}
error={key.length > 32}
helperText={key.length > 32 ? 'KEY长度不能超过32位' : ''}
></TextField>
<div className="h-3"></div>
<Alert
severity="warning"
className={css`
margin-bottom: 8px;
`}
>
该KEY仅用作和风天气插件使用,不会保存到服务器,请勿将KEY泄露给他人。
</Alert>
<TextField
fullWidth
variant="standard"
label="和风天气插件 KEY"
placeholder="请输入和风天气天气插件 KEY"
value={pluginKey}
disabled={!permission}
onChange={(e) => {
setPluginKey(e.target.value);
}}
onBlur={() => {
saveWeather({
userId,
weather: weather,
city: location,
pluginKey,
});
}}
error={pluginKey.length > 32}
helperText={pluginKey.length > 32 ? 'KEY长度不能超过32位' : ''}
></TextField>
</ItemAccordion>
<ContentTitle title="高级设置" />
<ItemCard
title="刷新时间"
desc="设置天气自动更新时间间隔"
action={
<Select
disabled={!key || !permission}
value={weatherInterval}
onChange={(e) => setWeatherInterval(e.target.value)}
options={refreshOptions}
/>
}
/>
<ItemCard
title="首页展示"
desc="设置首页是否展示天气"
action={
<Switch
disabled={!permission}
checked={show}
onChange={(e) => setShow(e.target.checked)}
/>
}
/>
</ContentList>
<ContentLinkList>
<ContentTitle title="相关链接" />
<Link text="和风天气开发平台" href="https://dev.qweather.com/" />
</ContentLinkList>
</div>
);
}
Example #20
Source File: index.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
OtherApis: React.FC<PageProps> = (props) => {
const { route, children } = props;
const [iconApi, setIconApi] = React.useState('');
const [apiStatus, setApiStatus] = React.useState<ApiStatus>({});
const init = () => {
const account = localStorage.getItem('account');
const data = getOtherIconApi({
userId: account ?? '',
type: 'icon',
});
setIconApi(data.apiId);
let map = {} as ApiStatus;
websiteIconApis.forEach((i) => {
map[i.id] = 'warning';
});
setApiStatus(map);
};
const onChange = (event: SelectChangeEvent<any>) => {
const select = event.target.value;
setIconApi(select);
const account = localStorage.getItem('account');
setOtherIconApi({
userId: account ?? '',
apiId: select,
type: 'icon',
});
};
const StatusChip = (status: string) => {
const statusMap = {
warning: (
<>
<PendingOutlined /> 等待响应
</>
),
success: (
<>
<Done /> 成功
</>
),
error: (
<>
<Close /> 失败
</>
),
};
return (
<Chip
size="small"
color={status as any}
label={
<div className="text-sm flex items-center gap-1">
{(statusMap as any)[status as any]}
</div>
}
/>
);
};
React.useEffect(() => {
init();
}, []);
return (
<div>
<ContentList>
<Alert severity="info">
<AlertTitle>提示</AlertTitle>
不同地区,不同网络下各API的表现可能不同,请选择最适合的API以提高使用体验。
</Alert>
<ItemAccordion
title="Website Icon API"
desc="设置获取网站图标的api"
action={
<Select
label="API"
value={iconApi}
size="small"
onChange={onChange}
options={websiteIconApis.map((i) => ({
label: i.name,
value: i.id,
}))}
/>
}
>
<div className="flex items-center text-sm gap-1 pb-2">
<PendingOutlined /> <span>等待响应</span>
<Done /> <span>成功</span>
<Close /> <span>失败</span> 状态仅作参考,具体以实际使用为准
</div>
{websiteIconApis.map((i) => {
return (
<AccordionDetailItem
key={i.id}
disabledRightPadding
title={i.name}
action={
<>
{StatusChip(apiStatus[i.id])}
<img
className={css`
display: none;
`}
src={`${i.url}google.com`}
alt={i.name}
onLoad={(v) => {
setApiStatus({ ...apiStatus, [i.id]: 'success' });
}}
onError={(err) => {
setApiStatus({ ...apiStatus, [i.id]: 'error' });
}}
/>
</>
}
/>
);
})}
</ItemAccordion>
</ContentList>
</div>
);
}
Example #21
Source File: index.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
Background: React.FC = () => {
const [value, setValue] = React.useState<BgOptions>({} as BgOptions); // 选择背景类型
const [selected, setSelected] = React.useState<AuthBackgroundType>('color');
const [account, setAccount] = React.useState<AuthData>({} as AuthData); // 当前账户
const [userBgSetting, setUserBgSetting] = React.useState<AuthBackground>(
{} as AuthBackground,
); // 当前账户的背景设置数据
const [expanded, setExpanded] = React.useState(false);
const bgOptions: BgOptions[] = [
{ label: '纯色', value: 'color', canSelect: true, autoExpaneded: false },
{
label: '必应壁纸',
value: 'random',
canSelect: true,
autoExpaneded: true,
},
{
label: '每日一图',
value: 'everyday',
canSelect: true,
autoExpaneded: false,
},
{ label: '在线图片', value: 'link', canSelect: true, autoExpaneded: true },
];
// 更新设置
const updateBgSetting = (id: string, setting: AuthBackground) => {
editAccount(id, {
background: setting,
});
};
// 选择背景类型
const handleChange = (event: SelectChangeEvent<any>) => {
const selected: AuthBackgroundType = event.target.value;
const data = bgOptions.find((i) => i.value === selected);
if (!data) return;
setSelected(selected);
setValue(data);
setExpanded(data.autoExpaneded);
if (data.canSelect === true) {
const setting = {
type: selected,
};
account._id && updateBgSetting(account._id, setting);
setUserBgSetting(setting);
}
};
// 初始化背景设置
const init = () => {
const data: AuthData = getAccount();
setAccount(data);
if (data && data.background) {
const type = data.background.type;
const option = bgOptions.find((i) => i.value === type);
setValue(option || bgOptions[0]);
setSelected(type || bgOptions[0].value);
setUserBgSetting(data.background);
} else {
data._id &&
updateBgSetting(data._id, {
type: bgOptions[0].value,
});
setValue(bgOptions[0]);
setSelected(bgOptions[0].value);
setUserBgSetting({ type: bgOptions[0].value });
}
};
React.useEffect(() => {
init();
}, []);
return (
<div>
<Example data={userBgSetting} />
<div className="flex gap-2 flex-col">
<Alert severity="info">
<AlertTitle>提示</AlertTitle>
近期必应在国内访问可能受阻,会导致图片无法加载,出现此情况非本网站原因。
</Alert>
<ItemAccordion
expanded={expanded}
onChange={(_, expanded) => {
setExpanded(expanded);
}}
title="个性化设置背景"
desc="背景设置主要适用于主页"
action={
<Select
label="背景类型"
value={selected}
size="small"
onChange={handleChange}
options={bgOptions}
/>
}
disableDetailPadding
>
{value.value === 'color' && (
<Alert severity="info">
设置为纯色背景,在纯色设置时,会自动应用当前主题配色。
</Alert>
)}
{value.value === 'random' && (
<Random
data={userBgSetting.data as AuthBackgroundRandomData}
onChange={(data) => {
if (userBgSetting.type === 'random') {
const setting = { ...userBgSetting, data };
setUserBgSetting(setting);
account._id && updateBgSetting(account._id, setting);
}
}}
/>
)}
{value.value === 'everyday' && (
<EveryDay data={userBgSetting.data as AuthBackgroundRandomData} />
)}
{value.value === 'link' && (
<Link
data={userBgSetting.data as AuthBackgroundLinkData}
onChange={(url) => {
if (userBgSetting.type === 'link') {
const data = { url };
const setting = { ...userBgSetting, data: data };
setUserBgSetting(setting);
account._id && updateBgSetting(account._id, setting);
}
}}
/>
)}
</ItemAccordion>
</div>
</div>
);
}
Example #22
Source File: index.tsx From ExpressLRS-Configurator with GNU General Public License v3.0 | 4 votes |
DeviceTargetForm: FunctionComponent<FirmwareVersionCardProps> = ({
onChange,
currentTarget,
deviceOptions,
firmwareVersionData,
}) => {
const [currentDevice, setCurrentDevice] = useState<Device | null>(null);
const [currentCategory, setCurrentCategory] = useState<string | null>(null);
const categorySelectOptions = useMemo(() => {
if (deviceOptions === null) {
return [];
}
return deviceOptions
.map((item) => item.category)
.filter((value, index, array) => array.indexOf(value) === index) // unique values
.map((category) => {
return {
label: category,
value: category,
};
})
.sort((a, b) => {
return a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1;
});
}, [deviceOptions]);
const deviceSelectOptions = useMemo(() => {
if (deviceOptions === null || currentCategory === null) {
return [];
}
return deviceOptions
.filter((item) => item.category === currentCategory)
.map((item) => {
return {
label: item.name,
value: item.name,
};
})
.sort((a, b) => {
return a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1;
});
}, [deviceOptions, currentCategory]);
// Used when currentTarget is changed from Network devices popup
useEffect(() => {
const device = deviceOptions?.find((item) =>
item.targets.find((target) => target.id === currentTarget?.id)
);
// verify that if there is a currentTarget that the category and device values match that target
if (device) {
if (currentCategory !== device.category) {
setCurrentCategory(device.category);
}
if (currentDevice?.name !== device.name) {
setCurrentDevice(device);
}
}
}, [currentTarget, currentCategory, currentDevice, deviceOptions]);
const onCategoryChange = useCallback(
(value: string | null) => {
if (value === currentCategory) {
return;
}
if (value === null) {
setCurrentCategory(null);
} else {
setCurrentCategory(value);
}
// When category changes, set the current target to null
setCurrentDevice(null);
onChange(null);
},
[onChange, currentCategory]
);
const onDeviceChange = useCallback(
(value: string | null) => {
if (value === null) {
setCurrentDevice(null);
onChange(null);
} else if (value !== currentDevice?.name) {
const device =
deviceOptions?.find((item) => item.name === value) ?? null;
setCurrentDevice(device);
const targets = sortDeviceTargets(device?.targets ?? []);
onChange(targets[0] ?? null);
}
},
[onChange, currentDevice, deviceOptions]
);
/*
Check if current device & category is present in deviceOptions. If not - reset to default state.
*/
useEffect(() => {
if (
deviceOptions === null ||
currentDevice === null ||
currentCategory === null
) {
return;
}
const category = deviceOptions?.find(
(item) => item.category === currentCategory
);
const device = deviceOptions?.find(
(item) => item.name === currentDevice?.name
);
if (!category && !device) {
onCategoryChange(null);
onDeviceChange(null);
} else if (category && !device) {
onDeviceChange(null);
}
}, [onCategoryChange, onDeviceChange, currentCategory, currentDevice]);
const onFlashingMethodChange = (value: Target | null) => {
onChange(value);
};
return (
<>
{currentDevice && !currentDevice.verifiedHardware && (
<Alert severity="warning" sx={styles.dangerZone}>
<AlertTitle>UNVERIFIED HARDWARE</AlertTitle>
The manufacturer of this hardware has not provided samples to the
developers for evaluation and verification, contact them for support
or proceed at your own risk. Not all features may work.
</Alert>
)}
{deviceOptions && deviceOptions?.length > 0 && (
<>
<Box sx={styles.root}>
<Omnibox
title="Device category"
currentValue={
categorySelectOptions.find(
(item) => item.value === currentCategory
) ?? null
}
onChange={onCategoryChange}
options={categorySelectOptions}
/>
</Box>
<Box sx={styles.root}>
<Omnibox
title="Device"
currentValue={
deviceSelectOptions.find(
(item) => item.value === currentDevice?.name
) ?? null
}
onChange={onDeviceChange}
options={deviceSelectOptions}
// if no category has been selected, disable the target select box
disabled={currentCategory === null}
/>
</Box>
</>
)}
{currentCategory && currentDevice && deviceOptions && (
<FlashingMethodOptions
onChange={onFlashingMethodChange}
currentTarget={currentTarget}
currentDevice={currentDevice}
firmwareVersionData={firmwareVersionData}
/>
)}
</>
);
}