@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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
 * 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&#8230;</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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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}
        />
      )}
    </>
  );
}