@mui/icons-material#ContentCopy TypeScript Examples

The following examples show how to use @mui/icons-material#ContentCopy. 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: 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 #2
Source File: TransactionPositions.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
function PositionTableRow({
    position,
    updatePosition,
    transactionAccounts,
    showAdvanced,
    copyPosition,
    updatePositionUsage,
    showAccountSelect,
    showAddAccount,
    deletePosition,
}) {
    const validateFloat = (value) => {
        return !(value === null || value === undefined || value === "" || isNaN(parseFloat(value)));
    };
    return (
        <>
            <TableCell key={`position-${position.id}-name`}>
                <WrappedTextField
                    key={`position-${position.id}-name`}
                    value={position.name}
                    id={`position-${position.id}-name`}
                    onChange={(value) => updatePosition(position, value, position.price, position.communist_shares)}
                    validate={(value) => value !== "" && value != null}
                />
            </TableCell>
            <TableCell key={`position-${position.id}-communist`} align="right">
                <WrappedTextField
                    key={`position-${position.id}-communist`}
                    id={`position-${position.id}-communist`}
                    value={position.price}
                    style={{ width: 70 }}
                    onChange={(value) =>
                        updatePosition(position, position.name, parseFloat(value), position.communist_shares)
                    }
                    validate={validateFloat}
                    errorMsg={"float required"}
                />
            </TableCell>
            {transactionAccounts.map((accountID) => (
                <TableCell align="right" key={accountID}>
                    {showAdvanced ? (
                        <ShareInput
                            value={
                                position.usages.hasOwnProperty(String(accountID))
                                    ? position.usages[String(accountID)]
                                    : 0
                            }
                            onChange={(value) => updatePositionUsage(position, accountID, value)}
                            inputProps={{ tabIndex: -1 }}
                        />
                    ) : (
                        <Checkbox
                            name={`${accountID}-checked`}
                            checked={position.usages.hasOwnProperty(String(accountID))}
                            onChange={(event) => updatePositionUsage(position, accountID, event.target.checked ? 1 : 0)}
                            inputProps={{ tabIndex: -1 }}
                        />
                    )}
                </TableCell>
            ))}
            {showAccountSelect && <TableCell></TableCell>}
            {showAddAccount && <TableCell></TableCell>}
            <TableCell align="right">
                {showAdvanced ? (
                    <ShareInput
                        value={position.communist_shares}
                        onChange={(value) => updatePosition(position, position.name, position.price, parseFloat(value))}
                        inputProps={{ tabIndex: -1 }}
                    />
                ) : (
                    <Checkbox
                        name="communist-checked"
                        checked={position.communist_shares !== 0}
                        onChange={(event) =>
                            updatePosition(position, position.name, position.price, event.target.checked ? 1 : 0)
                        }
                        inputProps={{ tabIndex: -1 }}
                    />
                )}
            </TableCell>
            <TableCell>
                <IconButton onClick={() => copyPosition(position)} tabIndex={-1}>
                    <ContentCopy />
                </IconButton>
                <IconButton onClick={() => deletePosition(position)} tabIndex={-1}>
                    <Delete />
                </IconButton>
            </TableCell>
        </>
    );
}
Example #3
Source File: AccountList.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function AccountList({ group }) {
    const [speedDialOpen, setSpeedDialOpen] = useState(false);
    const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);

    const [showPersonalAccountCreationModal, setShowPersonalAccountCreationModal] = useState(false);
    const [showClearingAccountCreationModal, setShowClearingAccountCreationModal] = useState(false);

    const [activeTab, setActiveTab] = useState("personal");
    const [searchValuePersonal, setSearchValuePersonal] = useState("");
    const [searchValueClearing, setSearchValueClearing] = useState("");

    const [showPersonalAccountEditModal, setShowPersonalAccountEditModal] = useState(false);
    const [showClearingAccountEditModal, setShowClearingAccountEditModal] = useState(false);
    const [clearingAccountToCopy, setClearingAccountToCopy] = useState(undefined);
    const [accountToEdit, setAccountToEdit] = useState(null);
    const [clearingAccountToEdit, setClearingAccountToEdit] = useState(null);
    const setAccounts = useSetRecoilState(groupAccounts(group.id));
    const personalAccounts = useRecoilValue(personalAccountsSeenByUser(group.id));
    const clearingAccounts = useRecoilValue(clearingAccountsSeenByUser(group.id));
    const allAccounts = useRecoilValue(accountsSeenByUser(group.id));
    const [accountToDelete, setAccountToDelete] = useState(null);
    const userPermissions = useRecoilValue(currUserPermissions(group.id));
    const currentUser = useRecoilValue(userData);
    const memberIDToUsername = useRecoilValue(groupMemberIDsToUsername(group.id));

    const [filteredPersonalAccounts, setFilteredPersonalAccounts] = useState([]);
    const [filteredClearingAccounts, setFilteredClearingAccounts] = useState([]);
    useEffect(() => {
        if (searchValuePersonal != null && searchValuePersonal !== "") {
            setFilteredPersonalAccounts(
                personalAccounts.filter((t) => {
                    return (
                        t.name.toLowerCase().includes(searchValuePersonal.toLowerCase()) ||
                        t.description.toLowerCase().includes(searchValuePersonal.toLowerCase())
                    );
                })
            );
        } else {
            return setFilteredPersonalAccounts(personalAccounts);
        }
    }, [personalAccounts, searchValuePersonal, setFilteredPersonalAccounts]);

    useEffect(() => {
        if (searchValueClearing != null && searchValueClearing !== "") {
            setFilteredClearingAccounts(
                clearingAccounts.filter((t) => {
                    return (
                        t.name.toLowerCase().includes(searchValueClearing.toLowerCase()) ||
                        t.description.toLowerCase().includes(searchValueClearing.toLowerCase())
                    );
                })
            );
        } else {
            return setFilteredClearingAccounts(clearingAccounts);
        }
    }, [clearingAccounts, searchValueClearing, setFilteredClearingAccounts]);

    useTitle(`${group.name} - Accounts`);

    const openAccountEdit = (account) => {
        setAccountToEdit(account);
        setShowPersonalAccountEditModal(true);
    };

    const closeAccountEdit = (evt, reason) => {
        if (reason !== "backdropClick") {
            setShowPersonalAccountEditModal(false);
            setAccountToEdit(null);
        }
    };

    const openClearingAccountEdit = (account) => {
        setClearingAccountToEdit(account);
        setShowClearingAccountEditModal(true);
    };

    const closeClearingAccountEdit = (evt, reason) => {
        if (reason !== "backdropClick") {
            setShowClearingAccountEditModal(false);
            setClearingAccountToEdit(null);
        }
    };

    const confirmDeleteAccount = () => {
        if (accountToDelete !== null) {
            deleteAccount({ accountID: accountToDelete })
                .then((account) => {
                    updateAccount(account, setAccounts);
                    setAccountToDelete(null);
                })
                .catch((err) => {
                    toast.error(err);
                });
        }
    };

    const openCreateDialog = () => {
        setClearingAccountToCopy(undefined);
        setShowClearingAccountCreationModal(true);
    };

    const copyClearingAccount = (account) => {
        setClearingAccountToCopy(account);
        setShowClearingAccountCreationModal(true);
    };

    return (
        <>
            <MobilePaper>
                <TabContext value={activeTab}>
                    <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
                        <TabList onChange={(e, newValue) => setActiveTab(newValue)} centered>
                            <Tab
                                value="personal"
                                label={
                                    <TextBadge badgeContent={personalAccounts.length} color="primary">
                                        <span>Personal Accounts</span>
                                    </TextBadge>
                                }
                            />
                            <Tab
                                label={
                                    <TextBadge badgeContent={clearingAccounts.length} color="primary">
                                        <span>Clearing Accounts</span>
                                    </TextBadge>
                                }
                                value="clearing"
                            />
                        </TabList>
                    </Box>
                    <TabPanel value="personal">
                        <List>
                            {personalAccounts.length === 0 ? (
                                <Alert severity="info">No Accounts</Alert>
                            ) : (
                                <>
                                    <ListItem>
                                        <Input
                                            value={searchValuePersonal}
                                            onChange={(e) => setSearchValuePersonal(e.target.value)}
                                            placeholder="Search…"
                                            inputProps={{
                                                "aria-label": "search",
                                            }}
                                            endAdornment={
                                                <InputAdornment position="end">
                                                    <IconButton
                                                        aria-label="clear search input"
                                                        onClick={(e) => setSearchValuePersonal("")}
                                                        edge="end"
                                                    >
                                                        <Clear />
                                                    </IconButton>
                                                </InputAdornment>
                                            }
                                        />
                                    </ListItem>
                                    <Divider />
                                    {filteredPersonalAccounts.map((account) => (
                                        <ListItem sx={{ padding: 0 }} key={account.id}>
                                            <ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
                                                <ListItemText
                                                    primary={
                                                        <div>
                                                            <span>{account.name}</span>
                                                            {account.owning_user_id === currentUser.id ? (
                                                                <span>
                                                                    , owned by{" "}
                                                                    <Chip
                                                                        size="small"
                                                                        component="span"
                                                                        color="primary"
                                                                        label="you"
                                                                    />
                                                                </span>
                                                            ) : (
                                                                account.owning_user_id !== null && (
                                                                    <span>
                                                                        , owned by{" "}
                                                                        <Chip
                                                                            size="small"
                                                                            component="span"
                                                                            color="secondary"
                                                                            label={
                                                                                memberIDToUsername[
                                                                                    account.owning_user_id
                                                                                ]
                                                                            }
                                                                        />
                                                                    </span>
                                                                )
                                                            )}
                                                        </div>
                                                    }
                                                    secondary={account.description}
                                                />
                                            </ListItemLink>
                                            {userPermissions.can_write && (
                                                <ListItemSecondaryAction>
                                                    <IconButton
                                                        color="primary"
                                                        onClick={() => openAccountEdit(account)}
                                                    >
                                                        <Edit />
                                                    </IconButton>
                                                    <IconButton
                                                        color="error"
                                                        onClick={() => setAccountToDelete(account.id)}
                                                    >
                                                        <Delete />
                                                    </IconButton>
                                                </ListItemSecondaryAction>
                                            )}
                                        </ListItem>
                                    ))}
                                </>
                            )}
                        </List>
                        {userPermissions.can_write && (
                            <>
                                <Grid container justifyContent="center">
                                    <Tooltip title="Create Personal Account">
                                        <IconButton
                                            color="primary"
                                            onClick={() => setShowPersonalAccountCreationModal(true)}
                                        >
                                            <Add />
                                        </IconButton>
                                    </Tooltip>
                                </Grid>
                                <CreateAccountModal
                                    show={showPersonalAccountCreationModal}
                                    onClose={(evt, reason) => {
                                        if (reason !== "backdropClick") {
                                            setShowPersonalAccountCreationModal(false);
                                        }
                                    }}
                                    group={group}
                                />
                                <EditAccountModal
                                    show={showPersonalAccountEditModal}
                                    onClose={closeAccountEdit}
                                    account={accountToEdit}
                                    group={group}
                                />
                            </>
                        )}
                    </TabPanel>
                    <TabPanel value="clearing">
                        <List>
                            {clearingAccounts.length === 0 ? (
                                <Alert severity="info">No Accounts</Alert>
                            ) : (
                                <>
                                    <ListItem>
                                        <Input
                                            value={searchValueClearing}
                                            onChange={(e) => setSearchValueClearing(e.target.value)}
                                            placeholder="Search…"
                                            inputProps={{
                                                "aria-label": "search",
                                            }}
                                            endAdornment={
                                                <InputAdornment position="end">
                                                    <IconButton
                                                        aria-label="clear search input"
                                                        onClick={(e) => setSearchValueClearing("")}
                                                        edge="end"
                                                    >
                                                        <Clear />
                                                    </IconButton>
                                                </InputAdornment>
                                            }
                                        />
                                    </ListItem>
                                    <Divider />
                                    {filteredClearingAccounts.map((account) => (
                                        <ListItem sx={{ padding: 0 }} key={account.id}>
                                            <ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
                                                <ListItemText primary={account.name} secondary={account.description} />
                                            </ListItemLink>
                                            {userPermissions.can_write && (
                                                <ListItemSecondaryAction>
                                                    <IconButton
                                                        color="primary"
                                                        onClick={() => openClearingAccountEdit(account)}
                                                    >
                                                        <Edit />
                                                    </IconButton>
                                                    <IconButton
                                                        color="primary"
                                                        onClick={() => copyClearingAccount(account)}
                                                    >
                                                        <ContentCopy />
                                                    </IconButton>
                                                    <IconButton
                                                        color="error"
                                                        onClick={() => setAccountToDelete(account.id)}
                                                    >
                                                        <Delete />
                                                    </IconButton>
                                                </ListItemSecondaryAction>
                                            )}
                                        </ListItem>
                                    ))}
                                </>
                            )}
                        </List>
                        {userPermissions.can_write && (
                            <>
                                <Grid container justifyContent="center">
                                    <Tooltip title="Create Clearing Account">
                                        <IconButton color="primary" onClick={openCreateDialog}>
                                            <Add />
                                        </IconButton>
                                    </Tooltip>
                                </Grid>
                                <CreateClearingAccountModal
                                    show={showClearingAccountCreationModal}
                                    onClose={(evt, reason) => {
                                        if (reason !== "backdropClick") {
                                            setShowClearingAccountCreationModal(false);
                                        }
                                    }}
                                    initialValues={clearingAccountToCopy}
                                    group={group}
                                />
                                <EditClearingAccountModal
                                    show={showClearingAccountEditModal}
                                    onClose={closeClearingAccountEdit}
                                    account={clearingAccountToEdit}
                                    group={group}
                                />
                            </>
                        )}
                    </TabPanel>
                </TabContext>
            </MobilePaper>
            {userPermissions.can_write && (
                <>
                    <SpeedDial
                        ariaLabel="Create Account"
                        sx={{ position: "fixed", bottom: 20, right: 20 }}
                        icon={<SpeedDialIcon />}
                        // onClose={() => setSpeedDialOpen(false)}
                        // onOpen={() => setSpeedDialOpen(true)}
                        onClick={toggleSpeedDial}
                        open={speedDialOpen}
                    >
                        <SpeedDialAction
                            icon={<PersonalAccountIcon />}
                            tooltipTitle="Personal"
                            tooltipOpen
                            onClick={() => setShowPersonalAccountCreationModal(true)}
                        />
                        <SpeedDialAction
                            icon={<ClearingAccountIcon />}
                            tooltipTitle="Clearing"
                            tooltipOpen
                            onClick={openCreateDialog}
                        />
                    </SpeedDial>

                    <Dialog maxWidth="xs" aria-labelledby="confirmation-dialog-title" open={accountToDelete !== null}>
                        <DialogTitle id="confirmation-dialog-title">Confirm delete account</DialogTitle>
                        <DialogContent dividers>
                            Are you sure you want to delete the account "
                            {allAccounts.find((acc) => acc.id === accountToDelete)?.name}"
                        </DialogContent>
                        <DialogActions>
                            <Button autoFocus onClick={() => setAccountToDelete(null)} color="primary">
                                Cancel
                            </Button>
                            <Button onClick={confirmDeleteAccount} color="error">
                                Ok
                            </Button>
                        </DialogActions>
                    </Dialog>
                </>
            )}
        </>
    );
}
Example #4
Source File: GroupInvites.tsx    From abrechnung with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function GroupInvites({ group }) {
    const [showModal, setShowModal] = useState(false);
    const invites = useRecoilValue(groupInvites(group.id));
    const members = useRecoilValue(groupMembers(group.id));
    const userPermissions = useRecoilValue(currUserPermissions(group.id));

    const isGuest = useRecoilValue(isGuestUser);

    useTitle(`${group.name} - Invite Links`);

    const deleteToken = (id) => {
        deleteGroupInvite({ groupID: group.id, inviteID: id }).catch((err) => {
            toast.error(err);
        });
    };

    const getMemberUsername = (member_id) => {
        const member = members.find((member) => member.user_id === member_id);
        if (member === undefined) {
            return "unknown";
        }
        return member.username;
    };

    const selectLink = (event) => {
        const node = event.target;
        const selection = window.getSelection();
        const range = document.createRange();
        range.selectNodeContents(node);
        selection.removeAllRanges();
        selection.addRange(range);
    };

    const copyToClipboard = (content) => {
        navigator.clipboard.writeText(content);
        toast.info("Link copied to clipboard!");
    };

    return (
        <MobilePaper>
            <Typography component="h3" variant="h5">
                Active Invite Links
            </Typography>
            {isGuest && (
                <Alert severity="info">
                    You are a guest user on this Abrechnung and therefore not permitted to create group invites.
                </Alert>
            )}
            <List>
                {invites.length === 0 ? (
                    <ListItem>
                        <ListItemText primary="No Links" />
                    </ListItem>
                ) : (
                    invites.map((invite) => (
                        <ListItem key={invite.id}>
                            <ListItemText
                                primary={
                                    invite.token === null ? (
                                        <span>token hidden, was created by another member</span>
                                    ) : (
                                        <span onClick={selectLink}>
                                            {window.location.origin}/invite/
                                            {invite.token}
                                        </span>
                                    )
                                }
                                secondary={
                                    <>
                                        {invite.description}, created by {getMemberUsername(invite.created_by)}, valid
                                        until{" "}
                                        {DateTime.fromISO(invite.valid_until).toLocaleString(DateTime.DATETIME_FULL)}
                                        {invite.single_use && ", single use"}
                                        {invite.join_as_editor && ", join as editor"}
                                    </>
                                }
                            />
                            {userPermissions.can_write && (
                                <ListItemSecondaryAction>
                                    <IconButton
                                        color="primary"
                                        onClick={() =>
                                            copyToClipboard(`${window.location.origin}/invite/${invite.token}`)
                                        }
                                    >
                                        <ContentCopy />
                                    </IconButton>
                                    <IconButton color="error" onClick={() => deleteToken(invite.id)}>
                                        <Delete />
                                    </IconButton>
                                </ListItemSecondaryAction>
                            )}
                        </ListItem>
                    ))
                )}
            </List>
            {userPermissions.can_write && !isGuest && (
                <>
                    <Grid container justifyContent="center">
                        <IconButton color="primary" onClick={() => setShowModal(true)}>
                            <Add />
                        </IconButton>
                    </Grid>
                    <InviteLinkCreate show={showModal} onClose={() => setShowModal(false)} group={group} />
                </>
            )}
        </MobilePaper>
    );
}