react-icons/fa#FaCheck TypeScript Examples

The following examples show how to use react-icons/fa#FaCheck. 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: Icons.tsx    From crosshare with GNU Affero General Public License v3.0 6 votes vote down vote up
CheckOrReveal = ({
  x,
  y,
  reveal,
}: {
  x: number;
  y: number;
  reveal: boolean;
}) => {
  if (reveal) {
    return (
      <FaEye x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
    );
  }
  return (
    <FaCheck x={x} y={y} size={32} fill="currentColor" stroke="currentColor" />
  );
}
Example #2
Source File: CapabilityLevel.tsx    From hub with Apache License 2.0 5 votes vote down vote up
CapabilityLevel = (props: Props) => {
  const activeLevel = OPERATOR_CAPABILITIES.findIndex(
    (level: string) => level === (props.capabilityLevel || '').toLowerCase()
  );
  if (isUndefined(props.capabilityLevel) || isNull(props.capabilityLevel) || activeLevel < 0) return null;

  return (
    <div>
      <div className="d-inline">
        <SmallTitle text="Capability Level" wrapperClassName="d-inline" />
        <CapabilityLevelInfoModal />
      </div>

      <div className="mb-3 position-relative">
        {OPERATOR_CAPABILITIES.map((level: string, index: number) => (
          <div
            key={`capabilityLevel-${index}`}
            className={`d-flex flex-row align-items-center my-2 position-relative stepWrapper ${styles.stepWrapper}`}
          >
            <div
              data-testid="capabilityLevelStep"
              className={classnames('rounded-circle text-center me-2 textLight step border bg-white', styles.step, {
                [`activeStep ${styles.activeStep}`]: activeLevel >= index,
              })}
            >
              {activeLevel >= index && <FaCheck />}
            </div>
            <small
              className={classnames('text-capitalize', {
                'text-muted': activeLevel < index,
              })}
            >
              {level}
            </small>
          </div>
        ))}
      </div>
    </div>
  );
}
Example #3
Source File: ClueSuggestionOverlay.tsx    From crosshare with GNU Affero General Public License v3.0 5 votes vote down vote up
NYTIcon = ({ row }: { row: any }) => {
  if (row?.n) {
    return <FaCheck />;
  }
  return <></>;
}
Example #4
Source File: Card.tsx    From hub with Apache License 2.0 4 votes vote down vote up
RepositoryCard = (props: Props) => {
  const history = useHistory();
  const { ctx } = useContext(AppCtx);
  const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
  const [transferModalStatus, setTransferModalStatus] = useState<boolean>(false);
  const [deletionModalStatus, setDeletionModalStatus] = useState<boolean>(false);
  const [badgeModalStatus, setBadgeModalStatus] = useState<boolean>(false);
  const dropdownMenu = useRef(null);
  const organizationName = ctx.prefs.controlPanel.selectedOrg;
  const hasErrors = !isUndefined(props.repository.lastTrackingErrors) && !isNull(props.repository.lastTrackingErrors);
  const hasScanningErrors =
    !isUndefined(props.repository.lastScanningErrors) && !isNull(props.repository.lastScanningErrors);
  const [openErrorsModal, setOpenErrorsModal] = useState<boolean>(false);
  const [openScanningErrorsModal, setOpenScanningErrorsModal] = useState<boolean>(false);

  const closeDropdown = () => {
    setDropdownMenuStatus(false);
  };

  useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);

  useEffect(() => {
    if (props.visibleModal) {
      if (props.visibleModal === 'scanning') {
        setOpenScanningErrorsModal(true);
      } else {
        setOpenErrorsModal(true);
      }
      history.replace({
        search: '',
      });
    }
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  const getLastTracking = (): JSX.Element => {
    const nextCheckTime: number = minutesToNearestInterval(30);

    if (isUndefined(props.repository.lastTrackingTs) || isNull(props.repository.lastTrackingTs)) {
      return (
        <>
          Not processed yet
          {props.repository.disabled
            ? '.'
            : nextCheckTime > 0
            ? `, it will be processed automatically in ~ ${nextCheckTime} minutes`
            : ', it will be processed automatically in less than 30 minutes'}
        </>
      );
    }

    const content = (
      <>
        {!isFuture(props.repository.lastTrackingTs!) && (
          <span>{moment.unix(props.repository.lastTrackingTs!).fromNow()}</span>
        )}
        {hasErrors ? (
          <>
            <FaExclamation className="mx-1 text-warning" />
            <RepositoryWarningModal />
          </>
        ) : (
          <FaCheck className="mx-1 text-success" />
        )}
      </>
    );

    let nextCheckMsg: string = '';
    if (nextCheckTime > 0 && !props.repository.disabled) {
      nextCheckMsg = `(it will be checked for updates again in ~ ${nextCheckTime} minutes)`;
    }

    if (hasErrors) {
      return (
        <>
          {content}
          <Modal
            modalDialogClassName={styles.modalDialog}
            modalClassName="mh-100"
            className={`d-inline-block ${styles.modal}`}
            buttonType={`ms-1 btn badge btn-outline-secondary ${styles.btn}`}
            buttonContent={
              <div className="d-flex flex-row align-items-center">
                <HiExclamation className="me-2" />
                <span className="d-none d-xl-inline d-xxl-none d-xxxl-inline">Show tracking errors log</span>
                <span className="d-inline d-xl-none d-xxl-inline d-xxxl-none">Logs</span>
              </div>
            }
            header={
              <div className={`h3 m-2 flex-grow-1 text-truncate ${styles.title}`}>
                Tracking errors log - {props.repository.displayName || props.repository.name}
              </div>
            }
            open={openErrorsModal}
            onClose={() => setOpenErrorsModal(false)}
            footerClassName={styles.modalFooter}
          >
            <div className="d-flex h-100 mw-100 overflow-hidden">
              <div className="d-flex flex-column w-100">
                <div className={`mb-2 ${styles.trackingTime}`}>
                  {moment.unix(props.repository.lastTrackingTs!).format('llll Z')}
                </div>
                <div
                  className={`position-relative flex-grow-1 mw-100 mh-100 overflow-hidden ${styles.modalSyntaxTrackerWrapper}`}
                >
                  {props.repository.lastTrackingErrors && (
                    <SyntaxHighlighter
                      language="bash"
                      style={tomorrowNight}
                      customStyle={{ fontSize: '90%', height: '100%' }}
                    >
                      {props.repository.lastTrackingErrors}
                    </SyntaxHighlighter>
                  )}
                </div>
              </div>
            </div>
          </Modal>
          <span className="ms-3 fst-italic text-muted">{nextCheckMsg}</span>
        </>
      );
    } else {
      return (
        <>
          {content}
          {openErrorsModal && (
            <Modal
              className={`d-inline-block ${styles.modal}`}
              header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Tracking errors log</div>}
              open
            >
              <div className="h5 text-center my-5 mw-100">
                It looks like the last tracking of this repository worked fine and no errors were produced.
                <br />
                <br />
                If you have arrived to this screen from an email listing some errors, please keep in mind those may have
                been already solved.
              </div>
            </Modal>
          )}
          <span className="ms-1 fst-italic text-muted">{nextCheckMsg}</span>
        </>
      );
    }
  };

  const getLastScanning = (): JSX.Element => {
    const nextCheckTime: number = minutesToNearestInterval(30, 15);

    if (
      props.repository.scannerDisabled ||
      isUndefined(props.repository.lastTrackingTs) ||
      isNull(props.repository.lastTrackingTs)
    )
      return <>-</>;

    if (isUndefined(props.repository.lastScanningTs) || isNull(props.repository.lastScanningTs)) {
      return (
        <>
          Not scanned yet
          {props.repository.disabled
            ? '.'
            : nextCheckTime > 0
            ? `, it will be scanned for security vulnerabilities in ~ ${nextCheckTime} minutes`
            : ', it will be scanned for security vulnerabilities in less than 30 minutes'}
        </>
      );
    }

    const content = (
      <>
        {!isFuture(props.repository.lastScanningTs!) && (
          <span>{moment.unix(props.repository.lastScanningTs!).fromNow()}</span>
        )}
        {hasScanningErrors ? (
          <FaExclamation className="mx-2 text-warning" />
        ) : (
          <FaCheck className="mx-2 text-success" />
        )}
      </>
    );

    let nextCheckMsg: string = '';
    if (nextCheckTime > 0 && !props.repository.disabled) {
      nextCheckMsg = `(it will be checked for updates again in ~ ${nextCheckTime} minutes)`;
    }

    if (hasScanningErrors) {
      return (
        <>
          {content}
          <Modal
            modalDialogClassName={styles.modalDialog}
            modalClassName="mh-100"
            className={`d-inline-block ${styles.modal}`}
            buttonType={`ms-1 btn badge btn-outline-secondary ${styles.btn}`}
            buttonContent={
              <div className="d-flex flex-row align-items-center">
                <HiExclamation className="me-2" />
                <span className="d-none d-sm-inline">Show scanning errors log</span>
                <span className="d-inline d-sm-none">Logs</span>
              </div>
            }
            header={
              <div className={`h3 m-2 flex-grow-1 text-truncate ${styles.title}`}>
                Scanning errors log - {props.repository.displayName || props.repository.name}
              </div>
            }
            open={openScanningErrorsModal}
            onClose={() => setOpenErrorsModal(false)}
            footerClassName={styles.modalFooter}
          >
            <div className="d-flex h-100 mw-100 overflow-hidden">
              <div className={`d-flex overflow-scroll ${styles.modalSyntaxWrapper}`}>
                {props.repository.lastScanningErrors && (
                  <SyntaxHighlighter
                    language="bash"
                    style={tomorrowNight}
                    customStyle={{ fontSize: '90%', height: '100%', marginBottom: '0' }}
                  >
                    {props.repository.lastScanningErrors}
                  </SyntaxHighlighter>
                )}
              </div>
            </div>
          </Modal>
          <span className="ms-3 fst-italic text-muted">{nextCheckMsg}</span>
        </>
      );
    } else {
      return (
        <>
          {content}
          {openScanningErrorsModal && (
            <Modal
              className={`d-inline-block ${styles.modal}`}
              header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Scanning errors log</div>}
              open
            >
              <div className="h5 text-center my-5 mw-100">
                It looks like the last security vulnerabilities scan of this repository worked fine and no errors were
                produced.
                <br />
                <br />
                If you have arrived to this screen from an email listing some errors, please keep in mind those may have
                been already solved.
              </div>
            </Modal>
          )}
          <span className="ms-1 fst-italic text-muted">{nextCheckMsg}</span>
        </>
      );
    }
  };

  return (
    <div className="col-12 col-xxl-6 py-sm-3 py-2 px-0 px-xxl-3" data-testid="repoCard">
      <div className="card h-100">
        <div className="card-body d-flex flex-column h-100">
          <div className="d-flex flex-row w-100 justify-content-between">
            <div className={`text-truncate h5 mb-0 ${styles.titleCard}`}>
              {props.repository.displayName || props.repository.name}
            </div>

            <OfficialBadge
              official={props.repository.official}
              className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
              type="repo"
            />

            <VerifiedPublisherBadge
              verifiedPublisher={props.repository.verifiedPublisher}
              className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
            />

            <DisabledRepositoryBadge
              disabled={props.repository.disabled!}
              className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
            />

            <ScannerDisabledRepositoryBadge
              scannerDisabled={props.repository.scannerDisabled!}
              className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
            />

            {transferModalStatus && (
              <TransferRepositoryModal
                open={true}
                repository={props.repository}
                onSuccess={props.onSuccess}
                onAuthError={props.onAuthError}
                onClose={() => setTransferModalStatus(false)}
              />
            )}

            {deletionModalStatus && (
              <DeletionModal
                repository={props.repository}
                organizationName={organizationName}
                setDeletionModalStatus={setDeletionModalStatus}
                onSuccess={props.onSuccess}
                onAuthError={props.onAuthError}
              />
            )}

            {badgeModalStatus && (
              <BadgeModal
                repository={props.repository}
                onClose={() => setBadgeModalStatus(false)}
                open={badgeModalStatus}
              />
            )}

            <div className="ms-auto ps-3">
              <RepositoryIconLabel kind={props.repository.kind} isPlural />
            </div>

            <div className="ms-3">
              <div
                ref={dropdownMenu}
                className={classnames('dropdown-menu dropdown-menu-end p-0', styles.dropdownMenu, {
                  show: dropdownMenuStatus,
                })}
              >
                <div className={`dropdown-arrow ${styles.arrow}`} />

                <button
                  className="dropdown-item btn btn-sm rounded-0 text-dark"
                  onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                    e.preventDefault();
                    closeDropdown();
                    setBadgeModalStatus(true);
                  }}
                  aria-label="Open badge modal"
                >
                  <div className="d-flex flex-row align-items-center">
                    <MdLabel className={`me-2 ${styles.btnIcon}`} />
                    <span>Get badge</span>
                  </div>
                </button>

                <ActionBtn
                  className="dropdown-item btn btn-sm rounded-0 text-dark"
                  onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                    e.preventDefault();
                    closeDropdown();
                    setTransferModalStatus(true);
                  }}
                  action={AuthorizerAction.TransferOrganizationRepository}
                  label="Open transfer repository modal"
                >
                  <>
                    <RiArrowLeftRightLine className={`me-2 ${styles.btnIcon}`} />
                    <span>Transfer</span>
                  </>
                </ActionBtn>

                <ActionBtn
                  className="dropdown-item btn btn-sm rounded-0 text-dark"
                  onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                    e.preventDefault();
                    closeDropdown();
                    props.setModalStatus({
                      open: true,
                      repository: props.repository,
                    });
                  }}
                  action={AuthorizerAction.UpdateOrganizationRepository}
                  label="Open update repository modal"
                >
                  <>
                    <FaPencilAlt className={`me-2 ${styles.btnIcon}`} />
                    <span>Edit</span>
                  </>
                </ActionBtn>

                <ActionBtn
                  className="dropdown-item btn btn-sm rounded-0 text-dark"
                  onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                    e.preventDefault();
                    closeDropdown();
                    setDeletionModalStatus(true);
                  }}
                  action={AuthorizerAction.DeleteOrganizationRepository}
                  label="Open delete repository modal"
                >
                  <>
                    <FaTrashAlt className={`me-2 ${styles.btnIcon}`} />
                    <span>Delete</span>
                  </>
                </ActionBtn>
              </div>

              <button
                className={`btn btn-outline-secondary rounded-circle p-0 text-center ${styles.btnDropdown}`}
                onClick={() => setDropdownMenuStatus(true)}
                aria-label="Open menu"
                aria-expanded={dropdownMenuStatus}
              >
                <BsThreeDotsVertical />
              </button>
            </div>
          </div>
          {props.repository.repositoryId && (
            <div className="mt-2 d-flex flex-row align-items-baseline">
              <div className="text-truncate">
                <small className="text-muted text-uppercase me-1">ID: </small>
                <small>{props.repository.repositoryId}</small>
              </div>
              <div className={`ms-1 ${styles.copyBtn}`}>
                <div className={`position-absolute ${styles.copyBtnWrapper}`}>
                  <ButtonCopyToClipboard
                    text={props.repository.repositoryId}
                    className="btn-link border-0 text-dark fw-bold"
                    label="Copy repository ID to clipboard"
                  />
                </div>
              </div>
            </div>
          )}
          <div className="text-truncate">
            <small className="text-muted text-uppercase me-1">Url: </small>
            <small>{props.repository.url}</small>
          </div>
          <div>
            <small className="text-muted text-uppercase me-1">Last processed: </small>
            <small>{getLastTracking()}</small>
          </div>
          <div>
            <small className="text-muted text-uppercase me-1">Last security scan: </small>
            <small>{getLastScanning()}</small>
          </div>

          <div className="mt-3 m-md-0 d-flex flex-row d-md-none">
            <OfficialBadge official={props.repository.official} className="me-3" type="repo" />
            <VerifiedPublisherBadge verifiedPublisher={props.repository.verifiedPublisher} className="me-3" />
            <DisabledRepositoryBadge disabled={props.repository.disabled!} />
          </div>
        </div>
      </div>
    </div>
  );
}
Example #5
Source File: Form.tsx    From hub with Apache License 2.0 4 votes vote down vote up
WebhookForm = (props: Props) => {
  const { ctx } = useContext(AppCtx);
  const form = useRef<HTMLFormElement>(null);
  const urlInput = useRef<RefInputField>(null);
  const contentTypeInput = useRef<RefInputField>(null);
  const [isSending, setIsSending] = useState(false);
  const [isValidated, setIsValidated] = useState(false);
  const [apiError, setApiError] = useState<string | null>(null);
  const [selectedPackages, setSelectedPackages] = useState<Package[]>(
    !isUndefined(props.webhook) && props.webhook.packages ? props.webhook.packages : []
  );
  const [eventKinds, setEventKinds] = useState<EventKind[]>(
    !isUndefined(props.webhook) ? props.webhook.eventKinds : [EventKind.NewPackageRelease]
  );
  const [isActive, setIsActive] = useState<boolean>(!isUndefined(props.webhook) ? props.webhook.active : true);
  const [contentType, setContentType] = useState<string>(
    !isUndefined(props.webhook) && props.webhook.contentType ? props.webhook.contentType : ''
  );
  const [template, setTemplate] = useState<string>(
    !isUndefined(props.webhook) && props.webhook.template ? props.webhook.template : ''
  );
  const [isAvailableTest, setIsAvailableTest] = useState<boolean>(false);
  const [currentTestWebhook, setCurrentTestWebhook] = useState<TestWebhook | null>(null);
  const [isTestSent, setIsTestSent] = useState<boolean>(false);
  const [isSendingTest, setIsSendingTest] = useState<boolean>(false);

  const getPayloadKind = (): PayloadKind => {
    let currentPayloadKind: PayloadKind = DEFAULT_PAYLOAD_KIND;
    if (!isUndefined(props.webhook) && props.webhook.contentType && props.webhook.template) {
      currentPayloadKind = PayloadKind.custom;
    }
    return currentPayloadKind;
  };

  const [payloadKind, setPayloadKind] = useState<PayloadKind>(getPayloadKind());

  const onCloseForm = () => {
    props.onClose();
  };

  const onContentTypeChange = (e: ChangeEvent<HTMLInputElement>) => {
    setContentType(e.target.value);
  };

  async function handleWebhook(webhook: Webhook) {
    try {
      setIsSending(true);
      if (isUndefined(props.webhook)) {
        await API.addWebhook(webhook, ctx.prefs.controlPanel.selectedOrg!);
      } else {
        await API.updateWebhook(webhook, ctx.prefs.controlPanel.selectedOrg!);
      }
      setIsSending(false);
      props.onSuccess();
      onCloseForm();
    } catch (err: any) {
      setIsSending(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        let error = compoundErrorMessage(
          err,
          `An error occurred ${isUndefined(props.webhook) ? 'adding' : 'updating'} the webhook`
        );
        if (!isUndefined(props.webhook) && err.kind === ErrorKind.Forbidden) {
          error = `You do not have permissions to ${isUndefined(props.webhook) ? 'add' : 'update'} the webhook ${
            isUndefined(props.webhook) ? 'to' : 'from'
          } the organization.`;
        }
        setApiError(error);
      } else {
        props.onAuthError();
      }
    }
  }

  async function triggerWebhookTest(webhook: TestWebhook) {
    try {
      setIsSendingTest(true);
      setIsTestSent(false);
      await API.triggerWebhookTest(webhook);
      setIsTestSent(true);
      setIsSendingTest(false);
    } catch (err: any) {
      setIsSendingTest(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        let error = compoundErrorMessage(err, `An error occurred testing the webhook`);
        setApiError(error);
      } else {
        props.onAuthError();
      }
    }
  }

  const triggerTest = () => {
    if (!isNull(currentTestWebhook)) {
      cleanApiError();
      triggerWebhookTest(currentTestWebhook);
    }
  };

  const submitForm = () => {
    if (form.current) {
      cleanApiError();
      const { isValid, webhook } = validateForm(form.current);
      if (isValid && !isNull(webhook)) {
        handleWebhook(webhook);
      }
    }
  };

  const validateForm = (form: HTMLFormElement): FormValidation => {
    let webhook: Webhook | null = null;
    const formData = new FormData(form);
    const isValid = form.checkValidity() && selectedPackages.length > 0;

    if (isValid) {
      webhook = {
        name: formData.get('name') as string,
        url: formData.get('url') as string,
        secret: formData.get('secret') as string,
        description: formData.get('description') as string,
        eventKinds: eventKinds,
        active: isActive,
        packages: selectedPackages,
      };

      if (payloadKind === PayloadKind.custom) {
        webhook = {
          ...webhook,
          template: template,
          contentType: contentType,
        };
      }

      if (props.webhook) {
        webhook = {
          ...webhook,
          webhookId: props.webhook.webhookId,
        };
      }
    }
    setIsValidated(true);
    return { isValid, webhook };
  };

  const addPackage = (packageItem: Package) => {
    const packagesList = [...selectedPackages];
    packagesList.push(packageItem);
    setSelectedPackages(packagesList);
  };

  const deletePackage = (packageId: string) => {
    const packagesList = selectedPackages.filter((item: Package) => item.packageId !== packageId);
    setSelectedPackages(packagesList);
  };

  const getPackagesIds = (): string[] => {
    return selectedPackages.map((item: Package) => item.packageId);
  };

  const updateEventKindList = (eventKind: EventKind) => {
    let updatedEventKinds: EventKind[] = [...eventKinds];
    if (eventKinds.includes(eventKind)) {
      // At least event kind must be selected
      if (updatedEventKinds.length > 1) {
        updatedEventKinds = eventKinds.filter((kind: EventKind) => kind !== eventKind);
      }
    } else {
      updatedEventKinds.push(eventKind);
    }
    setEventKinds(updatedEventKinds);
  };

  const cleanApiError = () => {
    if (!isNull(apiError)) {
      setApiError(null);
    }
  };

  const updateTemplate = (e: ChangeEvent<HTMLTextAreaElement>) => {
    setTemplate(e.target.value);
    checkTestAvailability();
  };

  const checkTestAvailability = () => {
    const formData = new FormData(form.current!);

    let webhook: TestWebhook = {
      url: formData.get('url') as string,
      eventKinds: eventKinds,
    };

    if (payloadKind === PayloadKind.custom) {
      webhook = {
        ...webhook,
        template: template,
        contentType: contentType,
      };
    }

    const isFilled = Object.values(webhook).every((x) => x !== null && x !== '');

    if (urlInput.current!.checkValidity() && isFilled) {
      setCurrentTestWebhook(webhook);
      setIsAvailableTest(true);
    } else {
      setCurrentTestWebhook(null);
      setIsAvailableTest(false);
    }
  };

  useEffect(() => {
    checkTestAvailability();
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  const getPublisher = (pkg: Package): JSX.Element => {
    return (
      <>
        {pkg.repository.userAlias || pkg.repository.organizationDisplayName || pkg.repository.organizationName}

        <small className="ms-2">
          (<span className={`text-uppercase text-muted d-none d-sm-inline ${styles.legend}`}>Repo: </span>
          <span className="text-dark">{pkg.repository.displayName || pkg.repository.name}</span>)
        </small>
      </>
    );
  };

  return (
    <div>
      <div className="mb-4 pb-2 border-bottom">
        <button
          className={`btn btn-link text-dark btn-sm ps-0 d-flex align-items-center ${styles.link}`}
          onClick={onCloseForm}
          aria-label="Back to webhooks list"
        >
          <IoIosArrowBack className="me-2" />
          Back to webhooks list
        </button>
      </div>

      <div className="mt-2">
        <form
          ref={form}
          data-testid="webhookForm"
          className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
          onClick={() => setApiError(null)}
          autoComplete="off"
          noValidate
        >
          <div className="d-flex">
            <div className="col-md-8">
              <InputField
                type="text"
                label="Name"
                labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
                name="name"
                value={!isUndefined(props.webhook) ? props.webhook.name : ''}
                invalidText={{
                  default: 'This field is required',
                }}
                validateOnBlur
                required
              />
            </div>
          </div>

          <div className="d-flex">
            <div className="col-md-8">
              <InputField
                type="text"
                label="Description"
                name="description"
                value={!isUndefined(props.webhook) ? props.webhook.description : ''}
              />
            </div>
          </div>

          <div>
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="url">
              Url<small className="ms-1 fst-italic">(Required)</small>
            </label>
            <div className="form-text text-muted mb-2 mt-0">
              A POST request will be sent to the provided URL when any of the events selected in the triggers section
              happens.
            </div>
            <div className="d-flex">
              <div className="col-md-8">
                <InputField
                  ref={urlInput}
                  type="url"
                  name="url"
                  value={!isUndefined(props.webhook) ? props.webhook.url : ''}
                  invalidText={{
                    default: 'This field is required',
                    typeMismatch: 'Please enter a valid url',
                  }}
                  onChange={checkTestAvailability}
                  validateOnBlur
                  required
                />
              </div>
            </div>
          </div>

          <div>
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="secret">
              Secret
            </label>
            <div className="form-text text-muted mb-2 mt-0">
              If you provide a secret, we'll send it to you in the <span className="fw-bold">X-ArtifactHub-Secret</span>{' '}
              header on each request. This will allow you to validate that the request comes from ArtifactHub.
            </div>
            <div className="d-flex">
              <div className="col-md-8">
                <InputField type="text" name="secret" value={!isUndefined(props.webhook) ? props.webhook.secret : ''} />
              </div>
            </div>
          </div>

          <div className="mb-3">
            <div className="form-check form-switch ps-0">
              <label htmlFor="active" className={`form-check-label fw-bold ${styles.label}`}>
                Active
              </label>
              <input
                id="active"
                type="checkbox"
                role="switch"
                className={`position-absolute ms-2 form-check-input ${styles.checkbox}`}
                value="true"
                onChange={() => setIsActive(!isActive)}
                checked={isActive}
              />
            </div>

            <div className="form-text text-muted mt-2">
              This flag indicates if the webhook is active or not. Inactive webhooks will not receive notifications.
            </div>
          </div>

          <div className="h4 pb-2 mt-4 mt-md-5 mb-4 border-bottom">Triggers</div>

          <div className="my-4">
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="kind" id="events-group">
              Events
            </label>

            <div role="group" aria-labelledby="events-group">
              {PACKAGE_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
                return (
                  <CheckBox
                    key={`check_${subs.kind}`}
                    name="eventKind"
                    value={subs.kind.toString()}
                    device="all"
                    label={subs.title}
                    checked={eventKinds.includes(subs.kind)}
                    onChange={() => {
                      updateEventKindList(subs.kind);
                      checkTestAvailability();
                    }}
                  />
                );
              })}
            </div>
          </div>

          <div className="mb-4">
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="packages" id="webhook-pkg-list">
              Packages<small className="ms-1 fst-italic">(Required)</small>
            </label>
            <div className="form-text text-muted mb-4 mt-0">
              When the events selected happen for any of the packages you've chosen, a notification will be triggered
              and the configured url will be called. At least one package must be selected.
            </div>
            <div className="mb-3 row">
              <div className="col-12 col-xxl-10 col-xxxl-8">
                <SearchPackages disabledPackages={getPackagesIds()} onSelection={addPackage} label="webhook-pkg-list" />
              </div>
            </div>

            {isValidated && selectedPackages.length === 0 && (
              <div className="invalid-feedback mt-0 d-block">At least one package has to be selected</div>
            )}

            {selectedPackages.length > 0 && (
              <div className="row">
                <div className="col-12 col-xxl-10 col-xxxl-8">
                  <table className={`table table-hover table-sm border transparentBorder text-break ${styles.table}`}>
                    <thead>
                      <tr className={styles.tableTitle}>
                        <th scope="col" className={`align-middle d-none d-sm-table-cell ${styles.fitCell}`}></th>
                        <th scope="col" className={`align-middle ${styles.packageCell}`}>
                          Package
                        </th>
                        <th scope="col" className="align-middle w-50 d-none d-sm-table-cell">
                          Publisher
                        </th>
                        <th scope="col" className={`align-middle ${styles.fitCell}`}></th>
                      </tr>
                    </thead>
                    <tbody className={styles.body}>
                      {selectedPackages.map((item: Package) => (
                        <tr key={`subs_${item.packageId}`} data-testid="packageTableCell">
                          <td className="align-middle text-center d-none d-sm-table-cell">
                            <RepositoryIcon kind={item.repository.kind} className={`${styles.icon} h-auto mx-2`} />
                          </td>
                          <td className="align-middle">
                            <div className="d-flex flex-row align-items-center">
                              <div
                                className={`d-flex align-items-center justify-content-center overflow-hidden p-1 rounded-circle border border-2 bg-white ${styles.imageWrapper} imageWrapper`}
                              >
                                <Image
                                  alt={item.displayName || item.name}
                                  imageId={item.logoImageId}
                                  className="mw-100 mh-100 fs-4"
                                  kind={item.repository.kind}
                                />
                              </div>

                              <div className={`ms-2 text-dark ${styles.cellWrapper}`}>
                                <div className="text-truncate">
                                  {item.displayName || item.name}
                                  <span className={`d-inline d-sm-none ${styles.legend}`}>
                                    <span className="mx-2">/</span>
                                    {getPublisher(item)}
                                  </span>
                                </div>
                              </div>
                            </div>
                          </td>
                          <td className="align-middle position-relative text-dark d-none d-sm-table-cell">
                            <div className={`d-table w-100 ${styles.cellWrapper}`}>
                              <div className="text-truncate">{getPublisher(item)}</div>
                            </div>
                          </td>

                          <td className="align-middle">
                            <button
                              className={`btn btn-link btn-sm mx-2 ${styles.closeBtn}`}
                              type="button"
                              onClick={(event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
                                event.preventDefault();
                                event.stopPropagation();
                                deletePackage(item.packageId);
                              }}
                              aria-label="Delete package from webhook"
                            >
                              <MdClose className="text-danger fs-5" />
                            </button>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </div>
            )}
          </div>

          <div className="h4 pb-2 mt-4 mt-md-5 mb-4 border-bottom">Payload</div>

          <div className="d-flex flex-row mb-3">
            {PAYLOAD_KINDS_LIST.map((item: PayloadKindsItem) => {
              return (
                <div className="form-check me-4" key={`payload_${item.kind}`}>
                  <input
                    className="form-check-input"
                    type="radio"
                    id={`payload_${item.kind}`}
                    name="payloadKind"
                    value={item.name}
                    checked={payloadKind === item.kind}
                    onChange={() => {
                      setPayloadKind(item.kind);
                      setIsValidated(false);
                      checkTestAvailability();
                    }}
                  />
                  <label className="form-check-label" htmlFor={`payload_${item.kind}`}>
                    {item.title}
                  </label>
                </div>
              );
            })}
          </div>

          {payloadKind === PayloadKind.custom && (
            <div className="lh-base">
              <div className="form-text text-muted mb-3">
                It's possible to customize the payload used to notify your service. This may help integrating
                ArtifactHub webhooks with other services without requiring you to write any code. To integrate
                ArtifactHub webhooks with Slack, for example, you could use a custom payload using the following
                template:
                <div className="my-3 w-100">
                  <div
                    className={`alert alert-light text-nowrap ${styles.codeWrapper}`}
                    role="alert"
                    aria-live="off"
                    aria-atomic="true"
                  >
                    {'{'}
                    <br />
                    <span className="ms-3">
                      {`"text": "Package`} <span className="fw-bold">{`{{ .Package.Name }}`}</span> {`version`}{' '}
                      <span className="fw-bold">{`{{ .Package.Version }}`}</span> released!{' '}
                      <span className="fw-bold">{`{{ .Package.URL }}`}</span>
                      {`"`}
                      <br />
                      {'}'}
                    </span>
                  </div>
                </div>
              </div>
            </div>
          )}

          <div className="d-flex">
            <div className="col-md-8">
              <InputField
                ref={contentTypeInput}
                type="text"
                label="Request Content-Type"
                name="contentType"
                value={contentType}
                placeholder={payloadKind === PayloadKind.default ? 'application/cloudevents+json' : 'application/json'}
                disabled={payloadKind === PayloadKind.default}
                required={payloadKind !== PayloadKind.default}
                invalidText={{
                  default: 'This field is required',
                }}
                onChange={(e: ChangeEvent<HTMLInputElement>) => {
                  onContentTypeChange(e);
                  checkTestAvailability();
                }}
              />
            </div>
          </div>

          <div className=" mb-4">
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="template">
              Template
            </label>

            {payloadKind === PayloadKind.custom && (
              <div className="form-text text-muted mb-4 mt-0">
                Custom payloads are generated using{' '}
                <ExternalLink
                  href="https://golang.org/pkg/text/template/"
                  className="fw-bold text-dark"
                  label="Open Go templates documentation"
                >
                  Go templates
                </ExternalLink>
                . Below you will find a list of the variables available for use in your template.
              </div>
            )}

            <div className="row">
              <div className="col col-xxl-10 col-xxxl-8">
                <AutoresizeTextarea
                  name="template"
                  value={payloadKind === PayloadKind.default ? DEFAULT_PAYLOAD_TEMPLATE : template}
                  disabled={payloadKind === PayloadKind.default}
                  required={payloadKind !== PayloadKind.default}
                  invalidText="This field is required"
                  minRows={6}
                  onChange={updateTemplate}
                />
              </div>
            </div>
          </div>

          <div className="mb-3">
            <label className={`form-label fw-bold ${styles.label}`} htmlFor="template">
              Variables reference
            </label>
            <div className="row">
              <div className="col col-xxxl-8 overflow-auto">
                <small className={`text-muted ${styles.tableWrapper}`}>
                  <table className={`table table-sm border ${styles.variablesTable}`}>
                    <tbody>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .BaseURL }}`}</span>
                        </th>
                        <td>Artifact Hub deployment base url.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Event.ID }}`}</span>
                        </th>
                        <td>Id of the event triggering the notification.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Event.Kind }}`}</span>
                        </th>
                        <td>
                          Kind of the event triggering notification. Possible values are{' '}
                          <span className="fw-bold">package.new-release</span> and{' '}
                          <span className="fw-bold">package.security-alert</span>.
                        </td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Name }}`}</span>
                        </th>
                        <td>Name of the package.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Version }}`}</span>
                        </th>
                        <td>Version of the new release.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.URL }}`}</span>
                        </th>
                        <td>ArtifactHub URL of the package.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes }}`}</span>
                        </th>
                        <td>List of changes this package version introduces.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes[i].Kind }}`}</span>
                        </th>
                        <td>
                          Kind of the change. Possible values are <span className="fw-bold">added</span>,{' '}
                          <span className="fw-bold">changed</span>, <span className="fw-bold">deprecated</span>,{' '}
                          <span className="fw-bold">removed</span>, <span className="fw-bold">fixed</span> and{' '}
                          <span className="fw-bold">security</span>. When the change kind is not provided, the value
                          will be empty.
                        </td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes[i].Description }}`}</span>
                        </th>
                        <td>Brief text explaining the change.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes[i].Links }}`}</span>
                        </th>
                        <td>List of links related to the change.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes[i].Links[i].Name }}`}</span>
                        </th>
                        <td>Name of the link.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Changes[i].Links[i].URL }}`}</span>
                        </th>
                        <td>Url of the link.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.ContainsSecurityUpdates }}`}</span>
                        </th>
                        <td>Boolean flag that indicates whether this package contains security updates or not.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Prerelease }}`}</span>
                        </th>
                        <td>Boolean flag that indicates whether this package version is a pre-release or not.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Repository.Kind }}`}</span>
                        </th>
                        <td>
                          Kind of the repository associated with the notification. Possible values are{' '}
                          <span className="fw-bold">falco</span>, <span className="fw-bold">helm</span>,{' '}
                          <span className="fw-bold">olm</span> and <span className="fw-bold">opa</span>.
                        </td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Repository.Name }}`}</span>
                        </th>
                        <td>Name of the repository.</td>
                      </tr>
                      <tr>
                        <th scope="row">
                          <span className="text-nowrap">{`{{ .Package.Repository.Publisher }}`}</span>
                        </th>
                        <td>
                          Publisher of the repository. If the owner is a user it'll be the user alias. If it's an
                          organization, it'll be the organization name.
                        </td>
                      </tr>
                    </tbody>
                  </table>
                </small>
              </div>
            </div>
          </div>

          <div className={`mt-4 mt-md-5 ${styles.btnWrapper}`}>
            <div className="d-flex flex-row justify-content-between">
              <div className="d-flex flex-row align-items-center me-3">
                <button
                  type="button"
                  className="btn btn-sm btn-success"
                  onClick={triggerTest}
                  disabled={!isAvailableTest || isSendingTest}
                  aria-label="Test webhook"
                >
                  {isSendingTest ? (
                    <>
                      <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                      <span className="ms-2">
                        Testing <span className="d-none d-md-inline"> webhook</span>
                      </span>
                    </>
                  ) : (
                    <div className="d-flex flex-row align-items-center text-uppercase">
                      <RiTestTubeFill className="me-2" />{' '}
                      <div>
                        Test <span className="d-none d-sm-inline-block">webhook</span>
                      </div>
                    </div>
                  )}
                </button>

                {isTestSent && (
                  <span className="text-success ms-2" data-testid="testWebhookTick">
                    <FaCheck />
                  </span>
                )}
              </div>

              <div className="ms-auto">
                <button
                  type="button"
                  className="btn btn-sm btn-outline-secondary me-3"
                  onClick={onCloseForm}
                  aria-label="Cancel"
                >
                  <div className="d-flex flex-row align-items-center text-uppercase">
                    <MdClose className="me-2" />
                    <div>Cancel</div>
                  </div>
                </button>

                <button
                  className="btn btn-sm btn-outline-secondary"
                  type="button"
                  disabled={isSending}
                  onClick={submitForm}
                  aria-label="Add webhook"
                >
                  {isSending ? (
                    <>
                      <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                      <span className="ms-2">{isUndefined(props.webhook) ? 'Adding' : 'Updating'} webhook</span>
                    </>
                  ) : (
                    <div className="d-flex flex-row align-items-center text-uppercase">
                      {isUndefined(props.webhook) ? (
                        <>
                          <MdAddCircle className="me-2" />
                          <span>Add</span>
                        </>
                      ) : (
                        <>
                          <FaPencilAlt className="me-2" />
                          <div>Save</div>
                        </>
                      )}
                    </div>
                  )}
                </button>
              </div>
            </div>

            <Alert message={apiError} type="danger" onClose={() => setApiError(null)} />
          </div>
        </form>
      </div>
    </div>
  );
}
Example #6
Source File: LastNotificationsModal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
LastNotificationsModal = (props: Props) => {
  const notificationsWithErrors: WebhookNotification[] = props.notifications.filter(
    (notif: WebhookNotification) => notif.error
  );

  return (
    <>
      <Modal
        className="d-inline-block"
        buttonType="btn badge btn-outline-secondary"
        buttonContent={
          <>
            <GrConnect className={`me-2 ${styles.icon}`} />
            <span>Show last notifications</span>
          </>
        }
        modalDialogClassName={styles.modalDialog}
        header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Last notifications</div>}
        footerClassName={styles.modalFooter}
      >
        <div className="m-3">
          <table className={`table table-striped table-bordered table-sm mb-0 ${styles.table}`}>
            <thead>
              <tr>
                <th scope="col">Notification id</th>
                <th scope="col">Created at</th>
                <th scope="col">Processed</th>
                <th scope="col">Processed at</th>
                <th scope="col">Succeeded</th>
              </tr>
            </thead>
            <tbody>
              {props.notifications.map((item: WebhookNotification) => (
                <tr data-testid="lastNotificationCell" key={`lastNotif_${item.notificationId}`}>
                  <td className="align-middle">{item.notificationId}</td>
                  <td className="align-middle">{moment.unix(item.createdAt).format('YYYY/MM/DD HH:mm:ss (Z)')}</td>
                  <td className="align-middle text-center">
                    {item.processed && <FaCheck className="text-success" data-testid="processedIcon" />}
                  </td>
                  <td className="align-middle">
                    {!isNull(item.processedAt) && moment.unix(item.processedAt).format('YYYY/MM/DD HH:mm:ss (Z)')}
                  </td>
                  <td className="align-middle text-center">
                    {item.processed && (
                      <>
                        {item.error ? (
                          <FaTimes className="text-danger" data-testid="failedIcon" />
                        ) : (
                          <FaCheck className="text-success" data-testid="succeededIcon" />
                        )}
                      </>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>

          {notificationsWithErrors.length > 0 && (
            <>
              <div className="h5 mt-5 mb-4 text-uppercase fw-bold">Errors logs</div>

              <table className={`table table-striped table-bordered table-sm mb-0 ${styles.table}`}>
                <thead>
                  <tr>
                    <th scope="col">Notification id</th>
                    <th scope="col">Error</th>
                  </tr>
                </thead>
                <tbody>
                  {notificationsWithErrors.map((item: WebhookNotification) => (
                    <tr data-testid="lastNotificationErrorCell" key={`lastNotifError_${item.notificationId}`}>
                      <td>{item.notificationId}</td>

                      <td>{item.error}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </>
          )}
        </div>
      </Modal>
      {notificationsWithErrors.length > 0 && (
        <FaExclamation className="ms-1 text-warning" data-testid="lastNotifAlert" />
      )}
    </>
  );
}
Example #7
Source File: SchemaDefinition.tsx    From hub with Apache License 2.0 4 votes vote down vote up
SchemaDefinition = (props: Props) => {
  const ref = useRef<HTMLDivElement>(null);
  const def = props.def.options[props.def.active];

  const getInitialType = (): string | undefined => {
    let currentType: string | undefined;
    if (def.type) {
      if (isArray(def.type)) {
        currentType = def.type[0] as string;
      } else {
        currentType = def.type;
      }
    } else {
      if (def.properties) {
        currentType = 'object';
      }
    }
    return currentType;
  };

  const [activeType, setActiveType] = useState<string | undefined>(getInitialType());

  useEffect(() => {
    // Scrolls content into view when a definition is expanded
    if (props.isExpanded && ref && ref.current) {
      ref.current!.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
    }
  }, [props.isExpanded, ref]);

  useEffect(() => {
    setActiveType(getInitialType());
  }, [props.def.active, props.def.combinationType]); /* eslint-disable-line react-hooks/exhaustive-deps */

  const typeDef = activeType ? SCHEMA_PROPS_PER_TYPE[activeType] : null;

  const formatPropValue = (value?: any, required?: string[]): JSX.Element => {
    if (isUndefined(value) || isNull(value)) {
      return <span className="ms-1">-</span>;
    }

    switch (typeof value) {
      case 'object':
        if (isArray(value)) {
          return (
            <div className="d-flex flex-column ms-3" role="list">
              {value.map((el: string) => (
                <div key={`it_${el}`} className={`${styles.listItem} position-relative`} role="listitem">
                  {el}
                </div>
              ))}
            </div>
          );
        } else {
          return (
            <div className="ms-3" role="list">
              {Object.keys(value).map((el: string) => (
                <div key={`it_${el}`} className={`${styles.listItem} position-relative`} role="listitem">
                  {el}:{' '}
                  <span className="text-muted">
                    {value[el] && value[el].type
                      ? isArray(value[el].type)
                        ? value[el].type.join(' | ')
                        : value[el].type
                      : '-'}
                  </span>{' '}
                  {(checkIfPropIsRequiredInSchema(el, def.required) || checkIfPropIsRequiredInSchema(el, required)) && (
                    <span className={`text-success text-uppercase position-relative ms-2 fw-bold ${styles.xsBadge}`}>
                      Required
                    </span>
                  )}
                </div>
              ))}
            </div>
          );
        }
      case 'boolean':
        return <span className="ms-1">{value.toString()}</span>;
      default:
        return <span className="ms-1">{value}</span>;
    }
  };

  const formatDefaultResume = () => {
    if (isNull(def.default)) return 'null';
    switch (typeof def.default) {
      case 'object':
        if (isEmpty(def.default)) {
          return <span className="ms-1">{props.defaultValue}</span>;
        } else {
          return (
            <>
              <FaCheck className="me-1 text-success" />
              <small>(please expand for more details)</small>
            </>
          );
        }
      default:
        return <span className="ms-1">{props.defaultValue}</span>;
    }
  };

  const getItemsDef = (value?: any): JSX.Element => {
    if (isUndefined(value) || isNull(value)) return <span className="ms-1">-</span>;
    const types = isArray(value) ? value.map((item: any) => item.type) : [value.type];
    return (
      <>
        <span className="ms-1">
          {`[${types.join(',')}] `}
          {def.uniqueItems && <span className="ms-1">(unique)</span>}
        </span>
      </>
    );
  };

  const getArrayDesc = (value?: any): JSX.Element => {
    if (isUndefined(value)) return <span className="ms-1">-</span>;
    const desc = isArray(value) ? value.map((item: any) => item.description) : [value.description];
    return <span>{detectLinksInText(desc.join('. '), 'text-break') || '-'}</span>;
  };

  const getTypeSpecificKeyword = (item: KeywordProp, className?: string): JSX.Element | null => {
    const value = (def as any)[item.value as string];
    return (
      <div
        className={classnames('d-flex align-items-baseline', className, {
          'flex-column': typeof value === 'object' && item.value !== 'items',
          'flex-row': typeof value !== 'object',
          'flex-wrap':
            item.value === 'items' && !isUndefined(value) && !isNull(value) && !isUndefined(value.properties),
        })}
      >
        <div>
          <small className="text-muted text-uppercase">{item.label}</small>:
        </div>
        {(() => {
          switch (item.value) {
            case 'items':
              return (
                <>
                  {getItemsDef(value)}
                  {!isUndefined(value) && !isNull(value) && value.type === 'object' && !isUndefined(value.properties) && (
                    <div className="w-100">
                      <div>
                        <small className="text-muted text-uppercase">Properties</small>:
                      </div>
                      {formatPropValue(value.properties, value.required)}
                    </div>
                  )}
                </>
              );
            default:
              return <>{formatPropValue(value)}</>;
          }
        })()}
      </div>
    );
  };

  const changeActivePath = () => {
    props.onActivePathChange(!props.isExpanded ? props.path : undefined);
  };

  return (
    <div className="position-relative w-100" ref={ref}>
      <div className={styles.contentWrapper}>
        <button
          className={`btn text-reset text-start p-0 position-relative w-100 ${styles.btn}`}
          onClick={changeActivePath}
          aria-label={`${props.isExpanded ? 'Hide' : 'Show'} detail`}
        >
          <div className="d-flex flex-column">
            <div className="d-flex flex-row align-items-start">
              <div className={`pe-2 text-secondary ${styles.icon}`}>
                {props.isExpanded ? <BsFillCaretDownFill /> : <BsFillCaretRightFill />}
              </div>
              <div className={`d-flex flex-column flex-grow-1 ${styles.content}`}>
                {props.def.error || isUndefined(activeType) ? (
                  <small className={`text-muted text-uppercase ${styles.errorMsg}`}>Raw</small>
                ) : (
                  <>
                    {def.title && (
                      <div
                        className={classnames('fw-bold text-truncate', {
                          [styles.titleWithRequiredLabel]: props.isRequired,
                        })}
                      >
                        {def.title}
                      </div>
                    )}

                    {!isNull(props.def.combinationType) && (
                      <>
                        <select
                          className={classnames(
                            'w-50 form-select',
                            styles.select,
                            { 'my-2': def.title },
                            { 'mb-2': isUndefined(def.title) }
                          )}
                          value={props.def.active}
                          onClick={(e: ReactMouseEvent<HTMLSelectElement, MouseEvent>) => e.stopPropagation()}
                          onChange={(e: ChangeEvent<HTMLSelectElement>) => {
                            props.setValue({
                              ...props.def,
                              active: parseInt(e.target.value),
                            });
                            if (!props.isExpanded) {
                              changeActivePath();
                            }
                          }}
                        >
                          {props.def.options.map((sch: JSONSchema, index: number) => (
                            <option value={index} key={`opt_${index}`}>
                              Option {index + 1}
                            </option>
                          ))}
                        </select>
                      </>
                    )}

                    <div className="d-flex flex-row align-items-start w-100">
                      <div className="text-nowrap">
                        <small className="text-muted text-uppercase">Type</small>:{' '}
                        {def.type && isArray(def.type) ? (
                          <select
                            aria-label="Type selection"
                            className={`ms-1 d-inline form-select position-relative ${styles.select} ${styles.selectInLine}`}
                            value={activeType}
                            onClick={(e: ReactMouseEvent<HTMLSelectElement, MouseEvent>) => e.stopPropagation()}
                            onChange={(e: ChangeEvent<HTMLSelectElement>) => {
                              setActiveType(e.target.value as string);
                              if (!props.isExpanded) {
                                changeActivePath();
                              }
                            }}
                          >
                            {(def.type as string[]).map((type: string, index: number) => (
                              <option value={type} key={`opt_${index}`}>
                                {type}
                              </option>
                            ))}
                          </select>
                        ) : (
                          <span className="ms-1 fw-bold">{activeType}</span>
                        )}
                        {activeType === 'array' && def.items && <>{getItemsDef(def.items)}</>}
                      </div>

                      {!isUndefined(def.default) && (
                        <div className="ms-3 text-truncate">
                          <small className="text-muted text-uppercase">Default</small>:{' '}
                          <span className={`text-truncate ${styles.default}`}>{formatDefaultResume()}</span>
                        </div>
                      )}

                      <div className="ms-auto ps-2">
                        {props.isRequired && (
                          <span
                            className={`badge badge-sm rounded-pill bg-success text-uppercase position-relative ${styles.badge}`}
                          >
                            Required
                          </span>
                        )}
                      </div>
                    </div>
                  </>
                )}
              </div>
            </div>
          </div>
        </button>

        {props.isExpanded && (
          <div className={`${styles.moreInfo} border-top my-2 pt-2`}>
            <div className="d-flex flex-column">
              {isNull(typeDef) ? (
                <div className="mt-2">
                  <SyntaxHighlighter language="json" style={tomorrowNight}>
                    {JSON.stringify(def, null, 2)}
                  </SyntaxHighlighter>
                </div>
              ) : (
                <>
                  <div className="d-flex flex-row align-items-between">
                    <div className="fw-bold mb-1">Annotations</div>
                    <div className="ms-auto">
                      <ButtonCopyToClipboard
                        text={props.path}
                        contentBtn="Copy path to clipboard"
                        className={`btn-link text-muted p-0 ${styles.btnClip}`}
                        visibleBtnText
                        label="Copy path to clipboard"
                      />
                    </div>
                  </div>
                  <div>
                    <small className="text-muted text-uppercase">Description</small>:{' '}
                    <span className="ms-1">
                      {def.description ? (
                        <span className="text-break">{detectLinksInText(def.description, styles.descriptionLink)}</span>
                      ) : (
                        <>
                          {def.type === 'array' && def.items ? (
                            <span className={styles.desc}>{getArrayDesc(def.items)}</span>
                          ) : (
                            <>-</>
                          )}
                        </>
                      )}
                    </span>
                  </div>

                  {!isUndefined(props.defaultValue) && typeof def.default === 'object' && !isEmpty(def.default) && (
                    <div>
                      <small className="text-muted text-uppercase">Default</small>: {props.defaultValue}
                    </div>
                  )}

                  {!isUndefined(typeDef) && (
                    <>
                      <div className="fw-bold mt-2 mb-1">Constraints</div>
                      {typeDef.map((keyword: KeywordProp) => (
                        <Fragment key={keyword.label}>
                          {isArray(keyword.value) ? (
                            <>
                              <div className="d-flex flex-row">
                                {keyword.value.map((subKeyword: KeywordProp) => (
                                  <Fragment key={subKeyword.label}>
                                    {getTypeSpecificKeyword(subKeyword, 'me-3')}
                                  </Fragment>
                                ))}
                              </div>
                            </>
                          ) : (
                            <>{getTypeSpecificKeyword(keyword)}</>
                          )}
                        </Fragment>
                      ))}
                      <div>
                        <small className="text-muted text-uppercase">Enum</small>: {formatPropValue(def.enum)}
                      </div>
                    </>
                  )}
                </>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
Example #8
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
IssueRequestsTable = (): JSX.Element => {
  const dispatch = useDispatch();
  const { t } = useTranslation();

  const queryParams = useQueryParams();
  const selectedIssueRequestId = queryParams.get(QUERY_PARAMETERS.ISSUE_REQUEST_ID);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.ISSUE_REQUESTS_PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();

  const { address, extensions, bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: issueRequestsTotalCountIdle,
    isLoading: issueRequestsTotalCountLoading,
    data: issueRequestsTotalCount,
    error: issueRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, issueCountQuery(`userParachainAddress_eq: "${address}"`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(issueRequestsTotalCountError);

  const {
    isIdle: issueRequestsIdle,
    isLoading: issueRequestsLoading,
    data: issueRequests,
    error: issueRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      ISSUE_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `userParachainAddress_eq: "${address}"` // `WHERE` condition
    ],
    issueFetcher
  );
  useErrorHandler(issueRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('issue_page.updated'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let date;
          if (issue.execution) {
            date = issue.execution.timestamp;
          } else if (issue.cancellation) {
            date = issue.cancellation.timestamp;
          } else {
            date = issue.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: `${t('issue_page.amount')} (${WRAPPED_TOKEN_SYMBOL})`,
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let wrappedTokenAmount;
          if (issue.execution) {
            wrappedTokenAmount = issue.execution.amountWrapped;
          } else {
            wrappedTokenAmount = issue.request.amountWrapped;
          }

          return <>{displayMonetaryAmount(wrappedTokenAmount)}</>;
        }
      },
      {
        Header: t('issue_page.btc_transaction'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issueRequest } }: any) {
          return (
            <>
              {issueRequest.backingPayment.btcTxId ? (
                <ExternalLink
                  href={`${BTC_EXPLORER_TRANSACTION_API}${issueRequest.backingPayment.btcTxId}`}
                  onClick={(event) => {
                    event.stopPropagation();
                  }}
                >
                  {shortTxId(issueRequest.backingPayment.btcTxId)}
                </ExternalLink>
              ) : issueRequest.status === IssueStatus.Expired || issueRequest.status === IssueStatus.Cancelled ? (
                t('redeem_page.failed')
              ) : (
                `${t('pending')}...`
              )}
            </>
          );
        }
      },
      {
        Header: t('issue_page.confirmations'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          const value = issue.backingPayment.confirmations;
          return <>{value === undefined ? t('not_applicable') : Math.max(value, 0)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: IssueStatus }) {
          let icon;
          let notice;
          let colorClassName;
          switch (value) {
            case IssueStatus.RequestedRefund:
            case IssueStatus.Completed: {
              icon = <FaCheck />;
              notice = t('completed');
              colorClassName = 'text-interlayConifer';
              break;
            }
            case IssueStatus.Cancelled:
            case IssueStatus.Expired: {
              icon = <FaRegTimesCircle />;
              notice = t('cancelled');
              colorClassName = 'text-interlayCinnabar';
              break;
            }
            default: {
              icon = <FaRegClock />;
              notice = t('pending');
              colorClassName = 'text-interlayCalifornia';
              break;
            }
          }

          // TODO: double-check with `src\components\UI\InterlayTable\StatusCell\index.tsx`
          return (
            <div className={clsx('inline-flex', 'items-center', 'space-x-1.5', colorClassName)}>
              {icon}
              <span>{notice}</span>
            </div>
          );
        }
      }
    ],
    [t]
  );

  const data =
    issueRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : issueRequests.map(
          // TODO: should type properly (`Relay`)
          (issueRequest: any) =>
            getIssueWithStatus(issueRequest, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    issueRequestsTotalCountIdle ||
    issueRequestsTotalCountLoading ||
    issueRequestsIdle ||
    issueRequestsLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (issueRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.ISSUE_REQUESTS_PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const handleIssueModalClose = () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.ISSUE_REQUEST_ID]: ''
    });
  };

  const handleRowClick = (requestId: string) => () => {
    if (extensions.length && address) {
      updateQueryParameters({
        [QUERY_PARAMETERS.ISSUE_REQUEST_ID]: requestId
      });
    } else {
      dispatch(showAccountModalAction(true));
    }
  };

  const totalSuccessfulIssueCount = issueRequestsTotalCount.data.issuesConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulIssueCount / TABLE_PAGE_LIMIT);
  // TODO: should type properly (`Relay`)
  const selectedIssueRequest = data.find((issueRequest: any) => issueRequest.id === selectedIssueRequestId);

  return (
    <>
      <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
        <SectionTitle>{t('issue_page.issue_requests')}</SectionTitle>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              const { className: rowClassName, ...restRowProps } = row.getRowProps();

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr
                  className={clsx(rowClassName, 'cursor-pointer')}
                  {...restRowProps}
                  onClick={handleRowClick(row.original.id)}
                >
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </InterlayTableContainer>
      {selectedIssueRequest && (
        <IssueRequestModal
          open={!!selectedIssueRequest}
          onClose={handleIssueModalClose}
          request={selectedIssueRequest}
        />
      )}
    </>
  );
}
Example #9
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
RedeemRequestsTable = (): JSX.Element => {
  const { t } = useTranslation();

  const queryParams = useQueryParams();
  const selectedRedeemRequestId = queryParams.get(QUERY_PARAMETERS.REDEEM_REQUEST_ID);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.REDEEM_REQUESTS_PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();

  const { address, bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: redeemRequestsTotalCountIdle,
    isLoading: redeemRequestsTotalCountLoading,
    data: redeemRequestsTotalCount,
    error: redeemRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, redeemCountQuery(`userParachainAddress_eq: "${address}"`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(redeemRequestsTotalCountError);

  const {
    isIdle: redeemRequestsIdle,
    isLoading: redeemRequestsLoading,
    data: redeemRequests,
    error: redeemRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      REDEEM_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `userParachainAddress_eq: "${address}"` // WHERE condition
    ],
    redeemFetcher
  );
  useErrorHandler(redeemRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('issue_page.updated'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          let date;
          if (redeem.execution) {
            date = redeem.execution.timestamp;
          } else if (redeem.cancellation) {
            date = redeem.cancellation.timestamp;
          } else {
            date = redeem.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: `${t('redeem_page.amount')} (${WRAPPED_TOKEN_SYMBOL})`,
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{displayMonetaryAmount(redeem.request.requestedAmountBacking)}</>;
        }
      },
      {
        Header: t('issue_page.btc_transaction'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeemRequest } }: any) {
          return (
            <>
              {redeemRequest.status === RedeemStatus.Expired ||
              redeemRequest.status === RedeemStatus.Retried ||
              redeemRequest.status === RedeemStatus.Reimbursed ? (
                t('redeem_page.failed')
              ) : (
                <>
                  {redeemRequest.backingPayment.btcTxId ? (
                    <ExternalLink
                      href={`${BTC_EXPLORER_TRANSACTION_API}${redeemRequest.backingPayment.btcTxId}`}
                      onClick={(event) => {
                        event.stopPropagation();
                      }}
                    >
                      {shortTxId(redeemRequest.backingPayment.btcTxId)}
                    </ExternalLink>
                  ) : (
                    `${t('pending')}...`
                  )}
                </>
              )}
            </>
          );
        }
      },
      {
        Header: t('issue_page.confirmations'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          const value = redeem.backingPayment.confirmations;
          return <>{value === undefined ? t('not_applicable') : Math.max(value, 0)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: RedeemStatus }) {
          let icon;
          let notice;
          let colorClassName;
          switch (value) {
            case RedeemStatus.Reimbursed: {
              icon = <FaCheck />; // TODO: should update according to the design
              notice = t('redeem_page.reimbursed');
              colorClassName = 'text-interlayConifer'; // TODO: should update according to the design
              break;
            }
            case RedeemStatus.Expired: {
              icon = <FaRegTimesCircle />;
              notice = t('redeem_page.recover');
              colorClassName = 'text-interlayCinnabar';
              break;
            }
            case RedeemStatus.Retried: {
              icon = <FaCheck />;
              notice = t('redeem_page.retried');
              colorClassName = 'text-interlayConifer';
              break;
            }
            case RedeemStatus.Completed: {
              icon = <FaCheck />;
              notice = t('completed');
              colorClassName = 'text-interlayConifer';
              break;
            }
            default: {
              icon = <FaRegClock />;
              notice = t('pending');
              colorClassName = 'text-interlayCalifornia';
              break;
            }
          }

          return (
            <div className={clsx('inline-flex', 'items-center', 'space-x-1.5', colorClassName)}>
              {icon}
              <span>{notice}</span>
            </div>
          );
        }
      }
    ],
    [t]
  );

  const data =
    redeemRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : redeemRequests.map(
          // TODO: should type properly (`Relay`)
          (redeem: any) =>
            getRedeemWithStatus(redeem, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    redeemRequestsIdle ||
    redeemRequestsLoading ||
    redeemRequestsTotalCountIdle ||
    redeemRequestsTotalCountLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (redeemRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUESTS_PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const handleRedeemModalClose = () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUEST_ID]: ''
    });
  };

  const handleRowClick = (requestId: string) => () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUEST_ID]: requestId
    });
  };

  const totalSuccessfulRedeemCount = redeemRequestsTotalCount.data.redeemsConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulRedeemCount / TABLE_PAGE_LIMIT);
  const selectedRedeemRequest = data.find((redeemRequest: any) => redeemRequest.id === selectedRedeemRequestId);

  return (
    <>
      <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
        <SectionTitle>{t('redeem_requests')}</SectionTitle>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              const { className: rowClassName, ...restRowProps } = row.getRowProps();

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr
                  className={clsx(rowClassName, 'cursor-pointer')}
                  {...restRowProps}
                  onClick={handleRowClick(row.original.id)}
                >
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </InterlayTableContainer>
      {selectedRedeemRequest && (
        <RedeemRequestModal
          open={!!selectedRedeemRequest}
          onClose={handleRedeemModalClose}
          request={selectedRedeemRequest}
        />
      )}
    </>
  );
}
Example #10
Source File: IssueRequests.tsx    From polkabtc-ui with Apache License 2.0 4 votes vote down vote up
function IssueRequests(): JSX.Element {
  const { address, extensions } = useSelector((state: StoreType) => state.general);
  const issueRequests = useSelector((state: StoreType) => state.issue.issueRequests).get(address) || [];
  const [issueModalOpen, setIssueModalOpen] = useState(false);
  const dispatch = useDispatch();
  const { t } = useTranslation();

  const handleIssueModalClose = () => {
    setIssueModalOpen(false);
  };

  const openWizard = () => {
    if (extensions.length && address) {
      setIssueModalOpen(true);
    } else {
      dispatch(showAccountModalAction(true));
    }
  };

  const handleCompleted = (status: IssueRequestStatus) => {
    switch (status) {
    case IssueRequestStatus.RequestedRefund:
    case IssueRequestStatus.Completed: {
      return <FaCheck className='inline-block' />;
    }
    case IssueRequestStatus.Cancelled:
    case IssueRequestStatus.Expired: {
      return (
        <Badge
          className='badge-style'
          variant='secondary'>
          {t('cancelled')}
        </Badge>
      );
    }
    default: {
      return <FaHourglass className='inline-block' />;
    }
    }
  };

  const requestClicked = (request: IssueRequest): void => {
    dispatch(changeIssueIdAction(request.id));
    openWizard();
  };

  return (
    <div
      className={clsx(
        'container',
        'mt-12',
        'mx-auto',
        'text-center'
      )}>
      {issueRequests?.length > 0 && (
        <>
          <h5
            className={clsx(
              'font-bold',
              'text-xl',
              'mb-2'
            )}>
            {t('issue_requests')}
          </h5>
          <p className='mb-4'>
            {t('issue_page.click_on_issue_request')}
          </p>
          <Table
            hover
            responsive
            size='md'>
            <thead>
              <tr>
                <th>{t('issue_page.updated')}</th>
                <th>{t('issue_page.amount')}</th>
                <th>{t('issue_page.btc_transaction')}</th>
                <th>{t('issue_page.confirmations')}</th>
                <th>{t('status')}</th>
              </tr>
            </thead>
            <tbody>
              {issueRequests.map((request: IssueRequest, index: number) => {
                return (
                  <tr
                    key={index}
                    onClick={() => requestClicked(request)}
                    className='table-row-opens-modal'>
                    <td>
                      {request.timestamp ?
                        formatDateTimePrecise(new Date(Number(request.timestamp))) :
                        t('pending')}
                    </td>
                    <td>
                      {request.issuedAmountBtc || request.requestedAmountPolkaBTC}{' '}
                      <span className='grey-text'>PolkaBTC</span>
                    </td>
                    <td>
                      <BitcoinTransaction
                        txId={request.btcTxId}
                        shorten />
                    </td>
                    <td>
                      {request.btcTxId === '' ?
                        t('not_applicable') :
                        Math.max(request.confirmations, 0)}
                    </td>
                    <td>{handleCompleted(request.status)}</td>
                  </tr>
                );
              })}
            </tbody>
          </Table>
          <IssueModal
            open={issueModalOpen}
            onClose={handleIssueModalClose} />
        </>
      )}
    </div>
  );
}
Example #11
Source File: RedeemRequests.tsx    From polkabtc-ui with Apache License 2.0 4 votes vote down vote up
export default function RedeemRequests(): ReactElement {
  const { address } = useSelector((state: StoreType) => state.general);
  const redeemRequests = useSelector((state: StoreType) => state.redeem.redeemRequests).get(address);
  const [showModal, setShowModal] = useState(false);
  const dispatch = useDispatch();
  const { t } = useTranslation();

  const closeModal = () => setShowModal(false);

  const handleStatusColumn = (request: RedeemRequest) => {
    switch (request.status) {
    case RedeemRequestStatus.Reimbursed: {
      return <div>{t('redeem_page.reimbursed')}</div>;
    }
    case RedeemRequestStatus.Expired: {
      return <div>{t('redeem_page.recover')}</div>;
    }
    case RedeemRequestStatus.Retried: {
      return <div>{t('redeem_page.retried')}</div>;
    }
    case RedeemRequestStatus.Completed: {
      return <FaCheck className='inline-block' />;
    }
    default: {
      return <FaHourglass className='inline-block' />;
    }
    }
  };

  const requestClicked = (request: RedeemRequest): void => {
    dispatch(changeRedeemIdAction(request.id));
    setShowModal(true);
  };

  return (
    <div
      className={clsx(
        'container',
        'mt-12',
        'mx-auto',
        'text-center'
      )}>
      {redeemRequests && redeemRequests.length > 0 && (
        <>
          <h5
            className={clsx(
              'font-bold',
              'text-xl',
              'mb-2'
            )}>
            {t('redeem_requests')}
          </h5>
          <Table
            hover
            responsive
            size='md'>
            <thead>
              <tr>
                <th>{t('issue_page.updated')}</th>
                <th>{t('issue_page.amount')}</th>
                <th>{t('issue_page.btc_transaction')}</th>
                <th>{t('issue_page.confirmations')}</th>
                <th>{t('status')}</th>
              </tr>
            </thead>
            <tbody>
              {redeemRequests &&
                redeemRequests.map(request => {
                  return (
                    <tr
                      key={request.id}
                      onClick={() => requestClicked(request)}
                      className='table-row-opens-modal'>
                      <td>
                        {request.timestamp ?
                          formatDateTimePrecise(new Date(Number(request.timestamp))) :
                          t('pending')}
                      </td>
                      <td>
                        {request.amountPolkaBTC}{' '}
                        <span className='grey-text'>PolkaBTC</span>
                      </td>
                      <td>
                        {request.status === RedeemRequestStatus.Expired ? (
                          <div>{t('redeem_page.failed')}</div>
                        ) : (
                          <BitcoinTransaction
                            txId={request.btcTxId}
                            shorten />
                        )}
                      </td>
                      <td>
                        {request.btcTxId === '' ?
                          t('not_applicable') :
                          Math.max(request.confirmations, 0)}
                      </td>
                      <td>{handleStatusColumn(request)}</td>
                    </tr>
                  );
                })}
            </tbody>
          </Table>
        </>
      )}
      <RedeemModal
        show={showModal}
        onClose={closeModal} />
    </div>
  );
}
Example #12
Source File: Puzzle.tsx    From crosshare with GNU Affero General Public License v3.0 4 votes vote down vote up
Puzzle = ({
  loadingPlayState,
  puzzle,
  play,
  ...props
}: PuzzleProps & AuthPropsOptional) => {
  const [state, dispatch] = useReducer(
    puzzleReducer,
    {
      type: 'puzzle',
      wasEntryClick: false,
      active: { col: 0, row: 0, dir: Direction.Across },
      grid: addClues(
        fromCells({
          mapper: (e) => e,
          width: puzzle.size.cols,
          height: puzzle.size.rows,
          cells: play
            ? play.g
            : puzzle.grid.map((s) => (s === BLOCK ? BLOCK : ' ')),
          vBars: new Set(puzzle.vBars),
          hBars: new Set(puzzle.hBars),
          allowBlockEditing: false,
          highlighted: new Set(puzzle.highlighted),
          highlight: puzzle.highlight,
          hidden: new Set(puzzle.hidden),
        }),
        puzzle.clues
      ),
      showExtraKeyLayout: false,
      answers: puzzle.grid,
      alternateSolutions: puzzle.alternateSolutions,
      verifiedCells: new Set<number>(play ? play.vc : []),
      wrongCells: new Set<number>(play ? play.wc : []),
      revealedCells: new Set<number>(play ? play.rc : []),
      downsOnly: play?.do || false,
      isEnteringRebus: false,
      rebusValue: '',
      success: play ? play.f : false,
      ranSuccessEffects: play ? play.f : false,
      filled: false,
      autocheck: false,
      dismissedKeepTrying: false,
      dismissedSuccess: false,
      moderating: false,
      showingEmbedOverlay: false,
      displaySeconds: play ? play.t : 0,
      bankedSeconds: play ? play.t : 0,
      ranMetaSubmitEffects: false,
      ...(play &&
        play.ct_rv && {
          contestRevealed: true,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      ...(play &&
        play.ct_sub && {
          ranMetaSubmitEffects: true,
          contestPriorSubmissions: play.ct_pr_subs,
          contestDisplayName: play.ct_n,
          contestSubmission: play.ct_sub,
          contestEmail: play.ct_em,
          contestSubmitTime: play.ct_t?.toMillis(),
        }),
      currentTimeWindowStart: 0,
      didCheat: play ? play.ch : false,
      clueView: false,
      cellsUpdatedAt: play ? play.ct : puzzle.grid.map(() => 0),
      cellsIterationCount: play ? play.uc : puzzle.grid.map(() => 0),
      cellsEverMarkedWrong: new Set<number>(play ? play.we : []),
      loadedPlayState: !loadingPlayState,
      waitToResize: true,
      isEditable(cellIndex) {
        return !this.verifiedCells.has(cellIndex) && !this.success;
      },
    },
    advanceActiveToNonBlock
  );

  const authContext = useContext(AuthContext);
  useEffect(() => {
    if (!authContext.notifications?.length) {
      return;
    }
    for (const notification of authContext.notifications) {
      if (notification.r) {
        // shouldn't be possible but be defensive
        continue;
      }
      if (!isNewPuzzleNotification(notification)) {
        continue;
      }
      if (notification.p === puzzle.id) {
        App.firestore()
          .collection('n')
          .doc(notification.id)
          .update({ r: true });
        return;
      }
    }
  }, [authContext.notifications, puzzle.id]);

  useEffect(() => {
    if (loadingPlayState === false) {
      const action: LoadPlayAction = {
        type: 'LOADPLAY',
        play: play,
        prefs: props.prefs,
        isAuthor: props.user ? props.user.uid === puzzle.authorId : false,
      };
      dispatch(action);
    }
  }, [loadingPlayState, play, props.user, props.prefs, puzzle.authorId]);

  // Every (unpaused) second dispatch a tick action which updates the display time
  useEffect(() => {
    function tick() {
      if (state.currentTimeWindowStart) {
        dispatch({ type: 'TICKACTION' });
      }
    }
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [state.currentTimeWindowStart, dispatch]);

  // Pause when page goes out of focus
  function prodPause() {
    if (process.env.NODE_ENV !== 'development') {
      dispatch({ type: 'PAUSEACTION' });
      writePlayToDBIfNeeded();
    }
  }
  useEventListener('blur', prodPause);

  const [muted, setMuted] = usePersistedBoolean('muted', false);
  const [toggleKeyboard, setToggleKeyboard] = usePersistedBoolean(
    'keyboard',
    false
  );

  // Set up music player for success song
  const [audioContext, initAudioContext] = useContext(CrosshareAudioContext);
  const playSuccess = useRef<(() => void) | null>(null);
  useEffect(() => {
    if (!audioContext) {
      return initAudioContext();
    }
    if (!playSuccess.current && !muted && audioContext) {
      fetch('/success.mp3')
        .then((response) => response.arrayBuffer())
        .then((buffer) => {
          audioContext.decodeAudioData(buffer, (audioBuffer) => {
            playSuccess.current = () => {
              const source = audioContext.createBufferSource();
              source.buffer = audioBuffer;
              source.connect(audioContext.destination);
              source.start();
            };
          });
        });
    }
  }, [muted, audioContext, initAudioContext]);

  const writePlayToDBIfNeeded = useCallback(
    async (user?: firebase.User) => {
      console.log('doing write play');
      if (!state.loadedPlayState) {
        return;
      }
      if (puzzle.contestAnswers?.length) {
        // For a meta we need to have run both to skip
        if (state.ranSuccessEffects && state.ranMetaSubmitEffects) {
          return;
        }
      } else {
        // For a reg puzzle skip if success effects have run
        if (state.ranSuccessEffects) {
          return;
        }
      }
      const u = user || props.user;
      if (!u) {
        return;
      }
      if (!isDirty(u, puzzle.id)) {
        return;
      }
      writePlayToDB(u, puzzle.id)
        .then(() => {
          console.log('Finished writing play state to db');
        })
        .catch((reason) => {
          console.error('Failed to write play: ', reason);
        });
    },
    [
      puzzle.id,
      puzzle.contestAnswers,
      props.user,
      state.ranMetaSubmitEffects,
      state.ranSuccessEffects,
      state.loadedPlayState,
    ]
  );

  const cachePlayForUser = useCallback(
    (user: firebase.User | undefined) => {
      if (!state.loadedPlayState) {
        return;
      }
      const updatedAt = TimestampClass.now();
      const playTime =
        state.currentTimeWindowStart === 0
          ? state.bankedSeconds
          : state.bankedSeconds +
            (new Date().getTime() - state.currentTimeWindowStart) / 1000;

      const playForUser: PlayWithoutUserT = {
        c: puzzle.id,
        n: puzzle.title,
        ua: updatedAt,
        g: Array.from(state.grid.cells),
        ct: Array.from(state.cellsUpdatedAt),
        uc: Array.from(state.cellsIterationCount),
        vc: Array.from(state.verifiedCells),
        wc: Array.from(state.wrongCells),
        we: Array.from(state.cellsEverMarkedWrong),
        rc: Array.from(state.revealedCells),
        t: playTime,
        ch: state.didCheat,
        do: state.downsOnly,
        f: state.success,
        ...(state.contestRevealed && {
          ct_rv: state.contestRevealed,
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
        }),
        ...(state.contestSubmission && {
          ct_sub: state.contestSubmission,
          ct_pr_subs: state.contestPriorSubmissions || [],
          ct_t:
            state.contestSubmitTime !== undefined
              ? TimestampClass.fromMillis(state.contestSubmitTime)
              : undefined,
          ct_n: state.contestDisplayName,
          ...(state.contestEmail && {
            ct_em: state.contestEmail,
          }),
        }),
      };
      cachePlay(user, puzzle.id, playForUser);
    },
    [
      state.downsOnly,
      state.loadedPlayState,
      puzzle.id,
      state.cellsEverMarkedWrong,
      state.cellsIterationCount,
      state.cellsUpdatedAt,
      state.didCheat,
      state.grid.cells,
      state.revealedCells,
      state.success,
      state.verifiedCells,
      state.wrongCells,
      puzzle.title,
      state.bankedSeconds,
      state.currentTimeWindowStart,
      state.contestSubmission,
      state.contestSubmitTime,
      state.contestEmail,
      state.contestDisplayName,
      state.contestRevealed,
      state.contestPriorSubmissions,
    ]
  );

  useEffect(() => {
    cachePlayForUser(props.user);
  }, [props.user, cachePlayForUser]);

  const router = useRouter();
  useEffect(() => {
    const listener = () => {
      writePlayToDBIfNeeded();
    };
    window.addEventListener('beforeunload', listener);
    router.events.on('routeChangeStart', listener);

    return () => {
      window.removeEventListener('beforeunload', listener);
      router.events.off('routeChangeStart', listener);
    };
  }, [writePlayToDBIfNeeded, router]);

  const { addToast } = useSnackbar();

  useEffect(() => {
    if (
      (state.contestSubmission || state.contestRevealed) &&
      !state.ranMetaSubmitEffects
    ) {
      const action: RanMetaSubmitEffectsAction = { type: 'RANMETASUBMIT' };
      dispatch(action);
      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }
    }
  }, [
    cachePlayForUser,
    state.contestSubmission,
    state.contestRevealed,
    state.ranMetaSubmitEffects,
    props.user,
    writePlayToDBIfNeeded,
  ]);

  useEffect(() => {
    if (state.success && !state.ranSuccessEffects) {
      const action: RanSuccessEffectsAction = { type: 'RANSUCCESS' };
      dispatch(action);

      if (props.user) {
        cachePlayForUser(props.user);
        writePlayToDBIfNeeded(props.user);
      } else {
        signInAnonymously().then((u) => {
          cachePlayForUser(u);
          writePlayToDBIfNeeded(u);
        });
      }

      let delay = 0;
      if (state.bankedSeconds <= 60) {
        addToast('? Solved in under a minute!');
        delay += 500;
      }
      if (!state.didCheat && state.downsOnly) {
        addToast('? Solved downs-only!', delay);
      } else if (!state.didCheat) {
        addToast('? Solved without check/reveal!', delay);
      }
      if (!muted && playSuccess.current) {
        playSuccess.current();
      }
    }
  }, [
    addToast,
    cachePlayForUser,
    muted,
    props.user,
    state.bankedSeconds,
    state.didCheat,
    state.downsOnly,
    state.ranSuccessEffects,
    state.success,
    writePlayToDBIfNeeded,
  ]);

  const physicalKeyboardHandler = useCallback(
    (e: KeyboardEvent) => {
      // Disable keyboard when paused / loading play
      if (!(state.success && state.dismissedSuccess)) {
        if (loadingPlayState || !state.currentTimeWindowStart) {
          return;
        }
      }

      const mkey = fromKeyboardEvent(e);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
        e.preventDefault();
      }
    },
    [
      dispatch,
      loadingPlayState,
      state.currentTimeWindowStart,
      state.success,
      state.dismissedSuccess,
    ]
  );
  useEventListener('keydown', physicalKeyboardHandler);

  const pasteHandler = useCallback(
    (e: ClipboardEvent) => {
      const tagName = (e.target as HTMLElement)?.tagName?.toLowerCase();
      if (tagName === 'textarea' || tagName === 'input') {
        return;
      }

      const pa: PasteAction = {
        type: 'PASTE',
        content: e.clipboardData?.getData('Text') || '',
      };
      dispatch(pa);
      e.preventDefault();
    },
    [dispatch]
  );
  useEventListener('paste', pasteHandler);

  let [entry, cross] = entryAndCrossAtPosition(state.grid, state.active);
  if (entry === null && cross !== null) {
    dispatch({ type: 'CHANGEDIRECTION' });
    [entry, cross] = [cross, entry];
  }

  const keyboardHandler = useCallback(
    (key: string) => {
      const mkey = fromKeyString(key);
      if (isSome(mkey)) {
        const kpa: KeypressAction = { type: 'KEYPRESS', key: mkey.value };
        dispatch(kpa);
      }
    },
    [dispatch]
  );

  const { acrossEntries, downEntries } = useMemo(() => {
    return {
      acrossEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Across
      ),
      downEntries: state.grid.entries.filter(
        (e) => e.direction === Direction.Down
      ),
    };
  }, [state.grid.entries]);

  const isEmbed = useContext(EmbedContext);

  /* `clueMap` is a map from ENTRYWORD => '5D: This is the clue' - we use this
   *    for comment clue tooltips. */
  const clueMap = useMemo(() => {
    return getEntryToClueMap(state.grid, state.answers);
  }, [state.grid, state.answers]);

  /* `refs` is a set of referenced entry indexes for each entry in the grid - we use this
   * for grid highlights when an entry is selected.
   *
   * `refPositions` is an array for each entry of [reffedEntry, clueTextStart, clueTextEnd] tuples
   */
  const [refs, refPositions] = useMemo(() => {
    return getRefs(state.grid);
  }, [state.grid]);

  const scrollToCross = useMatchMedia(SMALL_AND_UP_RULES);

  const overlayBaseProps: PuzzleOverlayBaseProps = {
    publishTime: puzzle.isPrivateUntil || puzzle.publishTime,
    coverImage: props.coverImage,
    profilePicture: props.profilePicture,
    downsOnly: state.downsOnly,
    clueMap: clueMap,
    user: props.user,
    nextPuzzle: props.nextPuzzle,
    puzzle: puzzle,
    isMuted: muted,
    solveTime: state.displaySeconds,
    didCheat: state.didCheat,
    dispatch: dispatch,
  };

  let puzzleView: ReactNode;

  const entryIdx = entryIndexAtPosition(state.grid, state.active);
  let refed: Set<number> = new Set();
  if (entryIdx !== null) {
    refed = refs[entryIdx] || new Set();
  }

  const shouldConceal =
    state.currentTimeWindowStart === 0 &&
    !(state.success && state.dismissedSuccess);
  if (state.clueView) {
    puzzleView = (
      <TwoCol
        left={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            isEnteringRebus={state.isEnteringRebus}
            rebusValue={state.rebusValue}
            wasEntryClick={state.wasEntryClick}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={true}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            scrollToCross={scrollToCross}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  } else {
    puzzleView = (
      <SquareAndCols
        leftIsActive={state.active.dir === Direction.Across}
        waitToResize={state.waitToResize}
        dispatch={dispatch}
        aspectRatio={state.grid.width / state.grid.height}
        square={(width: number, _height: number) => {
          return (
            <GridView
              isEnteringRebus={state.isEnteringRebus}
              rebusValue={state.rebusValue}
              squareWidth={width}
              grid={state.grid}
              active={state.active}
              entryRefs={refs}
              dispatch={dispatch}
              revealedCells={state.revealedCells}
              verifiedCells={state.verifiedCells}
              wrongCells={state.wrongCells}
              showAlternates={state.success ? state.alternateSolutions : null}
              answers={state.answers}
            />
          );
        }}
        header={
          <div
            css={{
              height: SQUARE_HEADER_HEIGHT,
              fontSize: 18,
              lineHeight: '24px',
              backgroundColor: 'var(--lighter)',
              overflowY: 'scroll',
              scrollbarWidth: 'none',
              display: 'flex',
            }}
          >
            {entry ? (
              <div css={{ margin: 'auto 1em' }}>
                <span
                  css={{
                    fontWeight: 'bold',
                    paddingRight: '0.5em',
                  }}
                >
                  {entry.labelNumber}
                  {entry.direction === Direction.Across ? 'A' : 'D'}
                </span>
                <span
                  css={{
                    color: shouldConceal ? 'transparent' : 'var(--text)',
                    textShadow: shouldConceal
                      ? '0 0 1em var(--conceal-text)'
                      : '',
                  }}
                >
                  <ClueText
                    refPositions={refPositions}
                    entryIndex={entry.index}
                    allEntries={state.grid.entries}
                    grid={state.grid}
                    downsOnly={state.downsOnly && !state.success}
                  />
                </span>
              </div>
            ) : (
              ''
            )}
          </div>
        }
        left={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Across`}
            entries={acrossEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
        right={
          <ClueList
            wasEntryClick={state.wasEntryClick}
            scrollToCross={scrollToCross}
            allEntries={state.grid.entries}
            refPositions={refPositions}
            refed={refed}
            dimCompleted={true}
            active={state.active}
            grid={state.grid}
            showEntries={false}
            conceal={shouldConceal}
            header={t`Down`}
            entries={downEntries}
            current={entry?.index}
            cross={cross?.index}
            dispatch={dispatch}
            downsOnly={state.downsOnly && !state.success}
          />
        }
      />
    );
  }

  const checkRevealMenus = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEye />} text={t`Reveal`}>
          {() => (
            <>
              <TopBarDropDownLink
                icon={<RevealSquare />}
                text={t`Reveal Square`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Square,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealEntry />}
                text={t`Reveal Word`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Entry,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
              <TopBarDropDownLink
                icon={<RevealPuzzle />}
                text={t`Reveal Puzzle`}
                onClick={() => {
                  const ca: CheatAction = {
                    type: 'CHEAT',
                    unit: CheatUnit.Puzzle,
                    isReveal: true,
                  };
                  dispatch(ca);
                }}
              />
            </>
          )}
        </TopBarDropDown>
        {!state.autocheck ? (
          <TopBarDropDown icon={<FaCheck />} text={t`Check`}>
            {() => (
              <>
                <TopBarDropDownLink
                  icon={<FaCheckSquare />}
                  text={t`Autocheck`}
                  onClick={() => {
                    const action: ToggleAutocheckAction = {
                      type: 'TOGGLEAUTOCHECK',
                    };
                    dispatch(action);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckSquare />}
                  text={t`Check Square`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Square,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckEntry />}
                  text={t`Check Word`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Entry,
                    };
                    dispatch(ca);
                  }}
                />
                <TopBarDropDownLink
                  icon={<CheckPuzzle />}
                  text={t`Check Puzzle`}
                  onClick={() => {
                    const ca: CheatAction = {
                      type: 'CHEAT',
                      unit: CheatUnit.Puzzle,
                    };
                    dispatch(ca);
                  }}
                />
              </>
            )}
          </TopBarDropDown>
        ) : (
          <TopBarLink
            icon={<FaCheckSquare />}
            text={t`Autochecking`}
            onClick={() => {
              const action: ToggleAutocheckAction = { type: 'TOGGLEAUTOCHECK' };
              dispatch(action);
            }}
          />
        )}
      </>
    ),
    [state.autocheck]
  );

  const moreMenu = useMemo(
    () => (
      <>
        <TopBarDropDown icon={<FaEllipsisH />} text={t`More`}>
          {() => (
            <>
              {!state.success ? (
                <TopBarDropDownLink
                  icon={<Rebus />}
                  text={t`Enter Rebus`}
                  shortcutHint={<EscapeKey />}
                  onClick={() => {
                    const kpa: KeypressAction = {
                      type: 'KEYPRESS',
                      key: { k: KeyK.Escape },
                    };
                    dispatch(kpa);
                  }}
                />
              ) : (
                ''
              )}
              {muted ? (
                <TopBarDropDownLink
                  icon={<FaVolumeUp />}
                  text={t`Unmute`}
                  onClick={() => setMuted(false)}
                />
              ) : (
                <TopBarDropDownLink
                  icon={<FaVolumeMute />}
                  text={t`Mute`}
                  onClick={() => setMuted(true)}
                />
              )}
              <TopBarDropDownLink
                icon={<FaKeyboard />}
                text={t`Toggle Keyboard`}
                onClick={() => setToggleKeyboard(!toggleKeyboard)}
              />
              {props.isAdmin ? (
                <>
                  <TopBarDropDownLink
                    icon={<FaGlasses />}
                    text="Moderate"
                    onClick={() => dispatch({ type: 'TOGGLEMODERATING' })}
                  />
                  <TopBarDropDownLinkA
                    href="/admin"
                    icon={<FaUserLock />}
                    text="Admin"
                  />
                </>
              ) : (
                ''
              )}
              {props.isAdmin || props.user?.uid === puzzle.authorId ? (
                <>
                  <TopBarDropDownLinkA
                    href={`/stats/${puzzle.id}`}
                    icon={<IoMdStats />}
                    text={t`Stats`}
                  />
                  <TopBarDropDownLinkA
                    href={`/edit/${puzzle.id}`}
                    icon={<FaEdit />}
                    text={t`Edit`}
                  />
                  {!isEmbed ? (
                    <TopBarDropDownLink
                      icon={<ImEmbed />}
                      text={t`Embed`}
                      onClick={() => dispatch({ type: 'TOGGLEEMBEDOVERLAY' })}
                    />
                  ) : (
                    ''
                  )}
                </>
              ) : (
                ''
              )}
              <TopBarDropDownLinkSimpleA
                href={'/api/pdf/' + puzzle.id}
                icon={<FaPrint />}
                text={t`Print Puzzle`}
              />
              {puzzle.hBars.length || puzzle.vBars.length ? (
                ''
              ) : (
                <TopBarDropDownLinkSimpleA
                  href={'/api/puz/' + puzzle.id}
                  icon={<FaRegFile />}
                  text={t`Download .puz File`}
                />
              )}
              <TopBarDropDownLinkA
                href="/account"
                icon={<FaUser />}
                text={t`Account / Settings`}
              />
              <TopBarDropDownLinkA
                href="/construct"
                icon={<FaHammer />}
                text={t`Construct a Puzzle`}
              />
            </>
          )}
        </TopBarDropDown>
      </>
    ),
    [
      muted,
      props.isAdmin,
      props.user?.uid,
      puzzle,
      setMuted,
      state.success,
      toggleKeyboard,
      setToggleKeyboard,
      isEmbed,
    ]
  );

  const description = puzzle.blogPost
    ? puzzle.blogPost.slice(0, 160) + '...'
    : puzzle.clues.map(getClueText).slice(0, 10).join('; ');

  const locale = router.locale || 'en';

  return (
    <>
      <Global
        styles={FULLSCREEN_CSS}
      />
      <Head>
        <title>{puzzle.title} | Crosshare crossword puzzle</title>
        <I18nTags
          locale={locale}
          canonicalPath={`/crosswords/${puzzle.id}/${slugify(puzzle.title)}`}
        />
        <meta key="og:title" property="og:title" content={puzzle.title} />
        <meta
          key="og:description"
          property="og:description"
          content={description}
        />
        <meta
          key="og:image"
          property="og:image"
          content={'https://crosshare.org/api/ogimage/' + puzzle.id}
        />
        <meta key="og:image:width" property="og:image:width" content="1200" />
        <meta key="og:image:height" property="og:image:height" content="630" />
        <meta
          key="og:image:alt"
          property="og:image:alt"
          content="An image of the puzzle grid"
        />
        <meta key="description" name="description" content={description} />
      </Head>
      <div
        css={{
          display: 'flex',
          flexDirection: 'column',
          height: '100%',
        }}
      >
        <div css={{ flex: 'none' }}>
          <TopBar title={puzzle.title}>
            {!loadingPlayState ? (
              !state.success ? (
                <>
                  <TopBarLink
                    icon={<FaPause />}
                    hoverText={t`Pause Game`}
                    text={timeString(state.displaySeconds, true)}
                    onClick={() => {
                      dispatch({ type: 'PAUSEACTION' });
                      writePlayToDBIfNeeded();
                    }}
                    keepText={true}
                  />
                  <TopBarLink
                    icon={state.clueView ? <SpinnerFinished /> : <FaListOl />}
                    text={state.clueView ? t`Grid` : t`Clues`}
                    onClick={() => {
                      const a: ToggleClueViewAction = {
                        type: 'TOGGLECLUEVIEW',
                      };
                      dispatch(a);
                    }}
                  />
                  {checkRevealMenus}
                  {moreMenu}
                </>
              ) : (
                <>
                  <TopBarLink
                    icon={<FaComment />}
                    text={
                      puzzle.contestAnswers?.length
                        ? !isMetaSolution(
                            state.contestSubmission,
                            puzzle.contestAnswers
                          ) && !state.contestRevealed
                          ? t`Contest Prompt / Submission`
                          : t`Comments / Leaderboard`
                        : t`Show Comments`
                    }
                    onClick={() => dispatch({ type: 'UNDISMISSSUCCESS' })}
                  />
                  {moreMenu}
                </>
              )
            ) : (
              moreMenu
            )}
          </TopBar>
        </div>
        {state.filled && !state.success && !state.dismissedKeepTrying ? (
          <KeepTryingOverlay dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.success && !state.dismissedSuccess ? (
          <PuzzleOverlay
            {...overlayBaseProps}
            overlayType={OverlayType.Success}
            contestSubmission={state.contestSubmission}
            contestHasPrize={puzzle.contestHasPrize}
            contestRevealed={state.contestRevealed}
            contestRevealDelay={puzzle.contestRevealDelay}
          />
        ) : (
          ''
        )}
        {state.moderating ? (
          <ModeratingOverlay puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.showingEmbedOverlay && props.user ? (
          <EmbedOverlay user={props.user} puzzle={puzzle} dispatch={dispatch} />
        ) : (
          ''
        )}
        {state.currentTimeWindowStart === 0 &&
        !state.success &&
        !(state.filled && !state.dismissedKeepTrying) ? (
          state.bankedSeconds === 0 ? (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Begin Puzzle`}
              message={t`Ready to get started?`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          ) : (
            <PuzzleOverlay
              {...overlayBaseProps}
              overlayType={OverlayType.BeginPause}
              dismissMessage={t`Resume`}
              message={t`Your puzzle is paused`}
              loadingPlayState={loadingPlayState || !state.loadedPlayState}
            />
          )
        ) : (
          ''
        )}
        <div
          css={{
            flex: '1 1 auto',
            overflow: 'scroll',
            scrollbarWidth: 'none',
            position: 'relative',
          }}
        >
          {puzzleView}
        </div>
        <div css={{ flex: 'none', width: '100%' }}>
          <Keyboard
            toggleKeyboard={toggleKeyboard}
            keyboardHandler={keyboardHandler}
            muted={muted}
            showExtraKeyLayout={state.showExtraKeyLayout}
            includeBlockKey={false}
          />
        </div>
      </div>
    </>
  );
}