react-icons/md#MdClose TypeScript Examples

The following examples show how to use react-icons/md#MdClose. 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: ModalLogin.tsx    From FaztCommunityMatch with MIT License 6 votes vote down vote up
CloseModalButton = styled(MdClose)`
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
padding: 0;
z-index: 4;
`
Example #2
Source File: UserInvitation.tsx    From hub with Apache License 2.0 5 votes vote down vote up
UserInvitation = (props: Props) => {
  const [orgToConfirm] = useState(props.orgToConfirm);
  const [isAccepting, setIsAccepting] = useState(false);
  const [validInvitation, setValidInvitation] = useState<boolean | null>(null);
  const [apiError, setApiError] = useState<string | null>(null);
  const history = useHistory();

  useEffect(() => {
    async function confirmOrganizationMembership() {
      setIsAccepting(true);
      try {
        await API.confirmOrganizationMembership(orgToConfirm!);
        setValidInvitation(true);
      } catch (err: any) {
        let error = !isUndefined(err.message)
          ? err.message
          : 'An error occurred accepting your invitation, please contact us about this issue.';
        if (err.kind === ErrorKind.Unauthorized) {
          error =
            'Please sign in to accept the invitation to join the organization. You can accept it from the Control Panel, in the organizations tab, or from the link you received in the invitation email.';
        }
        setApiError(error);
        setValidInvitation(false);
      } finally {
        setIsAccepting(false);
      }
    }

    if (!isUndefined(orgToConfirm)) {
      history.replace({
        pathname: '/',
        search: '',
      });
      confirmOrganizationMembership();
    }
  }, [orgToConfirm, history]);

  if (isUndefined(orgToConfirm)) return null;

  return (
    <Modal
      data-testid="userInvitationModal"
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Membership confirmation</div>}
      disabledClose={isAccepting}
      open={!isUndefined(orgToConfirm)}
    >
      <div
        className={`d-flex flex-column h-100 w-100 px-3 align-items-center justify-content-center text-center position-relative ${styles.content}`}
      >
        {isAccepting ? (
          <>
            <Loading className="position-relative" spinnerClassName="mt-0" />
            <small className="text-muted">Your are accepting the invitation...</small>
          </>
        ) : (
          <>
            {validInvitation ? (
              <>
                <MdDone className="display-4 text-success mb-4" />
                You have accepted the invitation to join the organization.
              </>
            ) : (
              <>
                <MdClose className="display-4 text-danger mb-4" />
                {apiError}
              </>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}
Example #3
Source File: UserConfirmation.tsx    From hub with Apache License 2.0 5 votes vote down vote up
UserConfirmation = (props: Props) => {
  const [emailCode] = useState(props.emailCode);
  const [verifying, setVerifying] = useState(false);
  const [validEmail, setValidEmail] = useState<boolean | null>(null);
  const [apiError, setApiError] = useState<string | null>(null);
  const history = useHistory();
  const siteName = getMetaTag('siteName');

  useEffect(() => {
    async function fetchEmailConfirmation() {
      setVerifying(true);
      try {
        await API.verifyEmail(emailCode!);
        setValidEmail(true);
      } catch (err: any) {
        let error = 'An error occurred verifying your email, please contact us about this issue.';
        if (!isUndefined(err.message)) {
          error = `Sorry, ${err.message}`;
        }
        setApiError(error);
        setValidEmail(false);
      } finally {
        setVerifying(false);
      }
    }

    if (!isUndefined(emailCode)) {
      history.replace({
        pathname: '/',
        search: '',
      });
      fetchEmailConfirmation();
    }
  }, [emailCode, history]);

  if (isUndefined(emailCode)) return null;

  return (
    <Modal
      data-testid="userConfirmationModal"
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Email confirmation</div>}
      disabledClose={verifying}
      modalClassName={styles.modal}
      open={!isUndefined(emailCode)}
    >
      <div
        className={`d-flex flex-column h-100 w-100 px-3 align-items-center justify-content-center text-center position-relative ${styles.content}`}
      >
        {verifying ? (
          <>
            <Loading className="position-relative" spinnerClassName="mt-0" />
            <small className="text-muted">We are verifying your email...</small>
          </>
        ) : (
          <>
            {validEmail ? (
              <>
                <MdDone className="display-4 text-success mb-4" />
                You email has been verified! Please, login to {siteName}.
              </>
            ) : (
              <>
                <MdClose className="display-4 text-danger mb-4" />
                {apiError}
              </>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}
Example #4
Source File: Modal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
Modal = (props: Props) => {
  const [openStatus, setOpenStatus] = useState(props.open || false);
  const ref = useRef<HTMLDivElement>(null);
  const unclosable = !isUndefined(props.unclosable) && props.unclosable;
  useOutsideClick(
    [ref, ...(!isUndefined(props.excludedRefs) ? [...props.excludedRefs] : [])],
    openStatus && !unclosable,
    () => {
      closeModal();
    }
  );
  useBodyScroll(openStatus, 'modal', props.breakPoint);

  const closeModal = () => {
    if (isUndefined(props.disabledClose) || !props.disabledClose) {
      setOpenStatus(false);
      if (!isUndefined(props.onClose)) {
        props.onClose();
      }
      if (props.error && !isNull(props.error) && props.cleanError) {
        props.cleanError();
      }
    }
  };

  useEffect(() => {
    if (!isUndefined(props.open)) {
      setOpenStatus(props.open);
    }
  }, [props.open]);

  return (
    <div className={props.className}>
      {!isUndefined(props.buttonContent) && (
        <div className={`position-relative ${styles.buttonWrapper}`}>
          <button
            type="button"
            className={classnames(
              'fw-bold text-uppercase position-relative btn w-100',
              styles.btn,
              { [`${props.buttonType}`]: !isUndefined(props.buttonType) },
              { 'btn-primary': isUndefined(props.buttonType) },
              { disabled: !isUndefined(props.disabledOpenBtn) && props.disabledOpenBtn }
            )}
            onClick={(e: MouseEvent<HTMLButtonElement>) => {
              e.preventDefault();
              if (isUndefined(props.disabledOpenBtn) || !props.disabledOpenBtn) {
                setOpenStatus(true);
              }
            }}
            aria-label="Open modal"
          >
            <div className="d-flex align-items-center justify-content-center">{props.buttonContent}</div>
          </button>
        </div>
      )}

      {openStatus && <div className={`modal-backdrop ${styles.activeBackdrop}`} data-testid="modalBackdrop" />}

      <div
        className={classnames('modal', styles.modal, { [`${styles.active} d-block`]: openStatus })}
        role="dialog"
        aria-modal={openStatus}
      >
        <div
          className={classnames(
            `modal-dialog modal-${props.size || 'lg'}`,
            { 'modal-dialog-centered modal-dialog-scrollable': isUndefined(props.noScrollable) || !props.noScrollable },
            props.modalDialogClassName
          )}
          ref={ref}
        >
          <div
            className={classnames('modal-content border border-3 mx-auto', styles.content, props.modalClassName, {
              [`position-relative ${styles.visibleContentBackdrop}`]:
                !isUndefined(props.visibleContentBackdrop) && props.visibleContentBackdrop,
            })}
          >
            {!isUndefined(props.header) && (
              <div
                className={`modal-header d-flex flex-row align-items-center ${styles.header} ${props.headerClassName}`}
              >
                {isString(props.header) ? <div className="modal-title h5">{props.header}</div> : <>{props.header}</>}

                {!unclosable && (
                  <button
                    type="button"
                    className="btn-close"
                    onClick={(e: MouseEvent<HTMLButtonElement>) => {
                      e.preventDefault();
                      closeModal();
                    }}
                    disabled={props.disabledClose}
                    aria-label="Close"
                  ></button>
                )}
              </div>
            )}

            <div className="modal-body p-4 h-100 d-flex flex-column">
              {openStatus && <>{props.children}</>}
              <div>
                <Alert message={props.error || null} type="danger" onClose={props.cleanError} />
              </div>
            </div>

            {(isUndefined(props.noFooter) || !props.noFooter) && (
              <div className={`modal-footer p-3 ${props.footerClassName}`}>
                {isUndefined(props.closeButton) ? (
                  <button
                    type="button"
                    className="btn btn-sm btn-outline-secondary text-uppercase"
                    onClick={(e: MouseEvent<HTMLButtonElement>) => {
                      e.preventDefault();
                      closeModal();
                    }}
                    disabled={props.disabledClose}
                    aria-label="Close modal"
                  >
                    <div className="d-flex flex-row align-items-center">
                      <MdClose className="me-2" />
                      <div>Close</div>
                    </div>
                  </button>
                ) : (
                  <>{props.closeButton}</>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
Example #5
Source File: TagsList.tsx    From hub with Apache License 2.0 4 votes vote down vote up
TagsList = (props: Props) => {
  const cleanRepeatedError = () => {
    if (props.repeatedTagNames) {
      props.setRepeatedTagNames(false);
    }
  };

  const deleteTag = (index: number) => {
    cleanRepeatedError();
    let updatedTags = [...props.tags];
    updatedTags.splice(index, 1);
    props.setContainerTags(updatedTags);
  };

  const addTag = () => {
    props.setContainerTags([...props.tags, { ...EMPTY_TAG, id: nanoid() }]);
  };

  const onUpdateTag = (index: number, field: 'name' | 'mutable', value?: string) => {
    cleanRepeatedError();
    let tagToUpdate: ContainerTag = props.tags[index];
    if (field === 'name') {
      tagToUpdate[field] = value as string;
    } else {
      tagToUpdate[field] = !tagToUpdate.mutable;
    }
    let updatedTags = [...props.tags];
    updatedTags[index] = tagToUpdate;
    props.setContainerTags(updatedTags);
  };

  useEffect(() => {
    if (isEmpty(props.tags)) {
      props.setContainerTags([{ ...EMPTY_TAG, id: nanoid() }]);
    }
  }, [props.tags]); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className="mb-4">
      <label className={`form-check-label fw-bold mb-2 ${styles.label}`}>
        Tags
        <button
          type="button"
          className={`btn btn-primary btn-sm ms-2 rounded-circle p-0 position-relative lh-1 ${styles.btn} ${styles.inTitle}`}
          onClick={addTag}
          disabled={props.tags.length === 10}
        >
          <HiPlus />
        </button>
      </label>

      {props.tags.length > 0 && (
        <>
          {props.tags.map((item: ContainerTag, idx: number) => {
            return (
              <div className="d-flex flex-row align-items-stretch justify-content-between" key={`tag_${item.id!}`}>
                <InputField
                  className="flex-grow-1"
                  type="text"
                  name={`tag_${idx}`}
                  autoComplete="off"
                  value={item.name}
                  placeholder="Tag name"
                  onChange={(e: ChangeEvent<HTMLInputElement>) => {
                    onUpdateTag(idx, 'name', e.target.value);
                  }}
                  smallBottomMargin
                />

                <div className="d-flex flex-row align-items-center mb-3 ms-3 flex-nowrap">
                  <div className={`ms-2 me-5 position-relative ${styles.inputWrapper}`}>
                    <div className="form-check form-switch ps-0">
                      <label htmlFor={`mutable_${idx}`} className={`form-check-label fw-bold ${styles.label}`}>
                        Mutable
                      </label>
                      <input
                        id={`mutable_${idx}`}
                        type="checkbox"
                        className="form-check-input position-absolute ms-2"
                        role="switch"
                        value="true"
                        checked={item.mutable}
                        onChange={() => {
                          onUpdateTag(idx, 'mutable');
                        }}
                      />
                    </div>
                  </div>

                  <div className={`position-relative text-end ${styles.btnWrapper}`}>
                    <button
                      className={`btn btn-danger btn-sm ms-auto rounded-circle p-0 position-relative lh-1 ${styles.btn}`}
                      type="button"
                      onClick={(event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
                        event.preventDefault();
                        event.stopPropagation();
                        deleteTag(idx);
                      }}
                      aria-label="Delete tag from repository"
                    >
                      <MdClose />
                    </button>
                  </div>
                </div>
              </div>
            );
          })}
        </>
      )}

      {props.repeatedTagNames && <div className="form-text text-danger mt-0">Tags names must be unique.</div>}

      <div className="form-text text-muted mt-2">
        The tags you'd like to list on Artifact Hub must be explicitly added. You can add up to{' '}
        <span className="fw-bold">10</span> (they can be edited later though). Mutable tags will be processed
        periodically, whereas immutable tags will be only processed once.
      </div>
    </div>
  );
}
Example #6
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 #7
Source File: AccountDeletion.tsx    From hub with Apache License 2.0 4 votes vote down vote up
AccountDeletion = (props: Props) => {
  const history = useHistory();
  const { ctx, dispatch } = useContext(AppCtx);
  const [code, setCode] = useState<string | undefined>(props.code);
  const [deleting, setDeleting] = useState<boolean>(false);
  const [validCode, setValidCode] = useState<boolean | null>(null);
  const [apiError, setApiError] = useState<string | null>(null);

  useEffect(() => {
    async function deleteUser() {
      setDeleting(true);
      try {
        await API.deleteUser(code!);
        setDeleting(false);
        setApiError(null);
        setValidCode(true);
      } catch {
        setDeleting(false);
        setApiError('Sorry, the code provided is not valid.');
        setValidCode(false);
      }
    }

    if (!isUndefined(ctx.user) && !isUndefined(code)) {
      history.replace({
        pathname: '/',
        search: '',
      });
      if (isNull(ctx.user)) {
        setCode(undefined);
        alertDispatcher.postAlert({
          type: 'warning',
          message: 'Please log in to complete your account deletion process.',
        });
      } else {
        deleteUser();
      }
    }
  }, [ctx]); /* eslint-disable-line react-hooks/exhaustive-deps */

  if (isUndefined(code) || isUndefined(ctx.user)) return null;

  return (
    <Modal
      data-testid="accountDeletionModal"
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Account deletion</div>}
      disabledClose={deleting}
      modalClassName={styles.modal}
      open={!isUndefined(code)}
      onClose={() => {
        setCode(undefined);
        if (validCode) {
          dispatch(signOut());
          history.push('/');
        }
      }}
    >
      <div
        className={`d-flex flex-column h-100 w-100 px-3 align-items-center justify-content-center text-center position-relative ${styles.content}`}
      >
        {deleting ? (
          <>
            <Loading className="position-relative" spinnerClassName="mt-0" />
            <small className="text-muted">We are deleting your account...</small>
          </>
        ) : (
          <>
            {validCode ? (
              <>
                <CgUserRemove className="display-4 text-dark mb-4" />
                You account has been successfully deleted. We're sorry to see you go, but you are always welcome back.
              </>
            ) : (
              <>
                <MdClose className="display-4 text-danger mb-4" />
                {apiError}
              </>
            )}
          </>
        )}
      </div>
    </Modal>
  );
}
Example #8
Source File: ResetPasswordModal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
ResetPasswordModal = (props: Props) => {
  const [code, setCode] = useState(props.code);
  const [verifying, setVerifying] = useState<boolean | null>(null);
  const [validCode, setValidCode] = useState<boolean | null>(null);
  const form = useRef<HTMLFormElement>(null);
  const passwordInput = useRef<RefInputField>(null);
  const repeatPasswordInput = useRef<RefInputField>(null);
  const [password, setPassword] = useState<Password>({ value: '', isValid: false });
  const [displayResetPwd, setDisplayResetPwd] = useState<boolean>(false);
  const history = useHistory();
  const [isSending, setIsSending] = useState<boolean>(false);
  const [isValidated, setIsValidated] = useState<boolean>(false);
  const [apiError, setApiError] = useState<string | null>(null);
  const [apiPwdError, setApiPwdError] = useState<string | null>(null);
  const [isSuccess, setIsSuccess] = useState<boolean>(false);

  const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
    setPassword({ value: e.target.value, isValid: e.currentTarget.checkValidity() });
  };

  // Clean API error when form is focused after validation
  const cleanApiError = () => {
    if (!isNull(apiError)) {
      setApiError(null);
    }
  };

  async function resetPassword(password: string) {
    try {
      await API.resetPassword(code!, password);
      setIsSuccess(true);
      setIsSending(false);
    } catch (err: any) {
      let error = compoundErrorMessage(err, 'An error occurred resetting the password');
      setApiError(error);
      setIsSending(false);
    }
  }

  const submitForm = () => {
    cleanApiError();
    setIsSending(true);
    if (form.current) {
      validateForm(form.current).then((validation: FormValidation) => {
        if (validation.isValid) {
          resetPassword(validation.password!);
          setIsValidated(true);
        } else {
          setIsSending(false);
        }
      });
    }
  };

  const validateForm = async (form: HTMLFormElement): Promise<FormValidation> => {
    let currentPassword: string | undefined;
    return validateAllFields().then((isValid: boolean) => {
      if (isValid) {
        const formData = new FormData(form);

        currentPassword = formData.get('password') as string;
      }
      setIsValidated(true);
      return { isValid, password: currentPassword };
    });
  };

  const validateAllFields = async (): Promise<boolean> => {
    return Promise.all([passwordInput.current!.checkIsValid(), repeatPasswordInput.current!.checkIsValid()]).then(
      (res: boolean[]) => {
        return every(res, (isValid: boolean) => isValid);
      }
    );
  };

  useEffect(() => {
    async function verifyPasswordResetCode() {
      setVerifying(true);
      try {
        await API.verifyPasswordResetCode(code!);
        setValidCode(true);
      } catch (err: any) {
        if (err.kind === ErrorKind.Gone) {
          setApiError('This password reset link is no longer valid, please get a new one.');
          setDisplayResetPwd(true);
        } else {
          let error = 'An error occurred with your password reset code.';
          if (!isUndefined(err.message)) {
            error = `Sorry, ${err.message}`;
          }
          setApiPwdError(error);
        }
        setValidCode(false);
      } finally {
        setVerifying(false);
      }
    }

    if (!isUndefined(code)) {
      history.replace({
        pathname: '/',
        search: '',
      });
      if (code !== props.code) {
        setCode(code);
      }
      verifyPasswordResetCode();
    }
  }, [code, history]); /* eslint-disable-line react-hooks/exhaustive-deps */

  if (isUndefined(code) || isNull(verifying)) return null;

  const closeButton = (
    <button
      className="btn btn-sm btn-outline-secondary"
      type="button"
      disabled={isSending}
      onClick={submitForm}
      aria-label="Reset password"
    >
      <div className="d-flex flex-row align-items-center">
        {isSending ? (
          <>
            <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
            <span className="ms-2">Resetting password...</span>
          </>
        ) : (
          <>
            <CgLastpass className="me-2" />
            <span className="text-uppercase">Reset password</span>
          </>
        )}
      </div>
    </button>
  );

  return (
    <Modal
      data-testid="resetPwdModal"
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Reset password</div>}
      disabledClose={verifying}
      modalClassName={styles.modal}
      open={!isUndefined(code)}
      closeButton={validCode && !isSuccess ? closeButton : undefined}
      error={apiError}
      cleanError={cleanApiError}
    >
      <div
        className={`d-flex flex-column h-100 w-100 align-items-center justify-content-center text-center position-relative ${styles.content}`}
      >
        {verifying ? (
          <>
            <Loading className="position-relative" spinnerClassName="mt-0" />
            <small className="text-muted">We are verifying your code...</small>
          </>
        ) : (
          <div className="text-start w-100">
            {validCode ? (
              <>
                {isSuccess ? (
                  <div className="d-flex flex-column text-center h5">
                    <div className="display-4 text-success mb-4">
                      <MdDone />
                    </div>
                    Your password has been reset successfully. You can now log in using the new credentials.
                  </div>
                ) : (
                  <form
                    ref={form}
                    data-testid="resetPwdForm"
                    className={classnames(
                      'w-100',
                      { 'needs-validation': !isValidated },
                      { 'was-validated': isValidated }
                    )}
                    onFocus={cleanApiError}
                    autoComplete="off"
                    noValidate
                  >
                    <InputField
                      ref={passwordInput}
                      type="password"
                      label="Password"
                      name="password"
                      minLength={6}
                      invalidText={{
                        default: 'This field is required',
                        customError: 'Insecure password',
                      }}
                      onChange={onPasswordChange}
                      autoComplete="new-password"
                      checkPasswordStrength
                      validateOnChange
                      validateOnBlur
                      required
                    />

                    <InputField
                      ref={repeatPasswordInput}
                      type="password"
                      label="Confirm password"
                      labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
                      name="confirmPassword"
                      pattern={password.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}
                      invalidText={{
                        default: 'This field is required',
                        patternMismatch: "Passwords don't match",
                      }}
                      autoComplete="new-password"
                      validateOnBlur={password.isValid}
                      required
                    />
                  </form>
                )}
              </>
            ) : (
              <>
                {displayResetPwd && <ResetPassword visibleTitle={false} onFinish={cleanApiError} />}
                {apiPwdError && (
                  <div className="d-flex flex-column text-center h5">
                    <div className="display-4 text-danger mb-4">
                      <MdClose />
                    </div>
                    {apiPwdError}
                  </div>
                )}
              </>
            )}
          </div>
        )}
      </div>
    </Modal>
  );
}
Example #9
Source File: Modal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
ChangelogModal = (props: Props) => {
  const history = useHistory();
  const btnsWrapper = useRef<HTMLDivElement>(null);
  const [openStatus, setOpenStatus] = useState<boolean>(false);
  const [changelog, setChangelog] = useState<ChangeLog[] | null | undefined>();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [currentPkgId, setCurrentPkgId] = useState<string | undefined>(undefined);
  const [activeVersionIndex, setActiveVersionIndex] = useState<number | undefined>(undefined);
  const [isGettingMd, setIsGettingMd] = useState<boolean>(false);

  const updateVersionInQueryString = (version?: string, index?: number) => {
    if (!isUndefined(index)) {
      updateActiveVersion(index);
    }
    history.replace({
      search: `?modal=changelog${version ? `&version=${version}` : ''}`,
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  useEffect(() => {
    setActiveVersionIndex(undefined);
    if (props.visibleChangelog && !isUndefined(currentPkgId)) {
      getChangelog();
    } else if (openStatus && !props.visibleChangelog) {
      onCloseModal(false);
    }
  }, [props.packageId, props.currentVersion]); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (props.visibleChangelog && !openStatus && isUndefined(currentPkgId)) {
      onOpenModal();
    }
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (btnsWrapper && btnsWrapper.current && !isUndefined(activeVersionIndex)) {
      const activeChild = btnsWrapper.current.children[activeVersionIndex];
      if (!isUndefined(activeChild)) {
        // Scroll to active button
        activeChild.scrollIntoView({ block: 'nearest' });
      }
    }
  }, [activeVersionIndex]); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    // We load correct active version after rendering modal
    if (openStatus && changelog && isUndefined(activeVersionIndex)) {
      const version = props.visibleVersion || props.currentVersion;
      if (version) {
        const currentIndex = changelog.findIndex((ch: ChangeLog) => ch.version === version);
        if (currentIndex >= 0) {
          if (version === props.currentVersion) {
            updateVersionInQueryString(version, currentIndex);
          } else {
            updateActiveVersion(currentIndex);
          }
        } else {
          updateVersionInQueryString();
          setActiveVersionIndex(0);
        }
        // If version doesn't exist
      } else {
        updateVersionInQueryString();
        setActiveVersionIndex(0);
      }
    }
  }, [openStatus, changelog]); /* eslint-disable-line react-hooks/exhaustive-deps */

  if (
    [RepositoryKind.Falco, RepositoryKind.Krew, RepositoryKind.HelmPlugin, RepositoryKind.Container].includes(
      props.repository.kind
    )
  )
    return null;

  async function getChangelog() {
    try {
      setIsLoading(true);
      setCurrentPkgId(props.packageId);
      setChangelog(await API.getChangelog(props.packageId));
      setIsLoading(false);
      setOpenStatus(true);
    } catch {
      setChangelog(null);
      alertDispatcher.postAlert({
        type: 'danger',
        message: 'An error occurred getting package changelog, please try again later.',
      });
      setIsLoading(false);
    }
  }

  const onOpenModal = () => {
    if (props.hasChangelog) {
      getChangelog();
    } else {
      history.replace({
        search: '',
        state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
      });
    }
  };

  const onCloseModal = (replaceUrl: boolean) => {
    setOpenStatus(false);
    setActiveVersionIndex(undefined);
    setIsGettingMd(false);
    setIsLoading(false);
    setChangelog(undefined);
    if (replaceUrl) {
      history.replace({
        search: '',
        state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
      });
    }
  };

  const updateActiveVersion = (versionIndex: number) => {
    if (versionIndex !== activeVersionIndex) {
      const element = document.getElementById(`changelog-${versionIndex}`);
      if (element) {
        element.scrollIntoView({ block: 'start' });
      }
      setActiveVersionIndex(versionIndex);
    }
  };

  const getChangelogMarkdown = () => {
    async function getChangelogMd() {
      try {
        setIsGettingMd(true);
        const markdown = await API.getChangelogMD({
          packageName: props.normalizedName,
          repositoryKind: getRepoKindName(props.repository.kind)!,
          repositoryName: props.repository.name,
        });

        const blob = new Blob([markdown], {
          type: 'text/markdown',
        });
        const link: HTMLAnchorElement = document.createElement('a');
        link.download = 'changelog.md';
        link.href = window.URL.createObjectURL(blob);
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        setIsGettingMd(false);
      } catch {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred getting package changelog markdown, please try again later.',
        });
        setIsGettingMd(false);
      }
    }
    getChangelogMd();
  };

  return (
    <>
      <ElementWithTooltip
        element={
          <button
            className={classnames('btn btn-outline-secondary btn-sm text-nowrap w-100', {
              disabled: !props.hasChangelog,
            })}
            onClick={onOpenModal}
            aria-label="Open Changelog modal"
            aria-disabled={!props.hasChangelog}
          >
            <div className="d-flex flex-row align-items-center justify-content-center text-uppercase">
              {isLoading ? (
                <>
                  <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                  <span className="d-none d-md-inline ms-2 fw-bold">Getting changelog...</span>
                </>
              ) : (
                <>
                  <CgFileDocument />
                  <span className="ms-2 fw-bold">Changelog</span>
                </>
              )}
            </div>
          </button>
        }
        visibleTooltip={!props.hasChangelog}
        tooltipWidth={230}
        tooltipClassName={styles.tooltip}
        tooltipMessage="No versions of this package include an annotation with information about the changes it introduces."
        active
      />

      {openStatus && changelog && (
        <Modal
          modalDialogClassName={styles.modalDialog}
          modalClassName="h-100"
          header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Changelog</div>}
          onClose={() => onCloseModal(true)}
          open={openStatus}
          closeButton={
            <div className="w-100 d-flex flex-row align-items-center justify-content-between">
              <button
                className="btn btn-sm btn-outline-secondary"
                onClick={getChangelogMarkdown}
                disabled={isGettingMd}
                aria-label="Get changelog markdown"
              >
                <div className="d-flex flex-row align-items-center">
                  {isGettingMd ? (
                    <>
                      <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                      <span className="ms-2">Getting markdown</span>
                    </>
                  ) : (
                    <>
                      <FaMarkdown className="me-2" />
                      <div className="text-uppercase">Get markdown</div>
                    </>
                  )}
                </div>
              </button>

              <button
                className="btn btn-sm btn-outline-secondary text-uppercase"
                onClick={() => onCloseModal(true)}
                aria-label="Close modal"
              >
                <div className="d-flex flex-row align-items-center">
                  <MdClose className="me-2" />
                  <div>Close</div>
                </div>
              </button>
            </div>
          }
        >
          <div className="mw-100 h-100">
            <div className="d-flex flex-row h-100">
              <div className="h-100 d-none d-lg-flex">
                <div
                  className={`d-flex flex-column me-4 border-end overflow-auto ${styles.versionsIndexWrapper}`}
                  ref={btnsWrapper}
                >
                  {changelog.map((item: ChangeLog, index: number) => {
                    if (isNull(item.changes) || isUndefined(item.changes)) return null;
                    return (
                      <div
                        data-testid="versionBtnWrapper"
                        className={classnames(
                          'pe-4 ps-2 position-relative border-bottom',
                          styles.versionBtnWrapper,
                          {
                            [styles.activeVersionBtnWrapper]: index === activeVersionIndex,
                          },
                          {
                            'border-top': index === 0,
                          }
                        )}
                        key={`opt_${item.version}`}
                      >
                        <button
                          className={classnames(
                            'btn btn-link text-dark text-start p-0 text-truncate position-relative w-100 rounded-0',
                            styles.versionBtn
                          )}
                          onClick={() => updateActiveVersion(index)}
                        >
                          {item.version}
                          <br />
                          <small className={`text-muted ${styles.legend}`}>
                            {moment.unix(item.ts).format('D MMM, YYYY')}
                          </small>
                        </button>
                      </div>
                    );
                  })}
                </div>
              </div>
              <Content
                changelog={changelog}
                onCloseModal={onCloseModal}
                updateVersionInQueryString={updateVersionInQueryString}
                normalizedName={props.normalizedName}
                activeVersionIndex={activeVersionIndex || 0}
                setActiveVersionIndex={setActiveVersionIndex}
                repository={props.repository}
                searchUrlReferer={props.searchUrlReferer}
                fromStarredPage={props.fromStarredPage}
              />
            </div>
          </div>
        </Modal>
      )}
    </>
  );
}
Example #10
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
ChartTemplatesModal = (props: Props) => {
  const history = useHistory();
  const [openStatus, setOpenStatus] = useState<boolean>(false);
  const [templates, setTemplates] = useState<ChartTemplate[] | null | undefined>();
  const [values, setValues] = useState<any | undefined>();
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [currentPkgId, setCurrentPkgId] = useState<string>(props.packageId);
  const [currentVersion, setCurrentVersion] = useState<string>(props.version);
  const [enabledDiff, setEnabledDiff] = useState<boolean>(!isUndefined(props.compareVersionTo));
  const [comparedVersion, setComparedVersion] = useState<string>(props.compareVersionTo || '');
  const [visibleDropdown, setVisibleDropdown] = useState<boolean>(false);
  const ref = useRef(null);
  const versionsWrapper = useRef<HTMLDivElement>(null);
  useOutsideClick([ref], visibleDropdown, () => setVisibleDropdown(false));

  const cleanUrl = () => {
    history.replace({
      search: '',
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  const updateUrl = (q: TemplatesQuery) => {
    history.replace({
      search: `?modal=template${q.template ? `&template=${q.template}` : ''}${
        q.compareTo ? `&compare-to=${q.compareTo}` : ''
      }`,
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  const onVersionChange = (version: string) => {
    setComparedVersion(version);
    updateUrl({ template: props.visibleTemplate, compareTo: version });
    setVisibleDropdown(false);
    setEnabledDiff(true);
  };

  useEffect(() => {
    if (props.visibleChartTemplates) {
      if (!openStatus && props.repoKind === RepositoryKind.Helm) {
        onOpenModal();
      } else {
        cleanUrl();
      }
    }
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (props.packageId !== currentPkgId && openStatus) {
      setOpenStatus(false);
    }
  }, [props.packageId]); /* eslint-disable-line react-hooks/exhaustive-deps */

  if (props.repoKind !== RepositoryKind.Helm) return null;

  async function getChartTemplates() {
    try {
      setIsLoading(true);
      setCurrentPkgId(props.packageId);
      setCurrentVersion(props.version);

      const data = await API.getChartTemplates(props.packageId, props.version);
      if (data && data.templates) {
        const formattedTemplates = formatTemplates(data.templates);
        if (formattedTemplates.length > 0) {
          setTemplates(formattedTemplates);
          setValues(data ? { Values: { ...data.values } } : null);
          setOpenStatus(true);
        } else {
          setTemplates(null);
          alertDispatcher.postAlert({
            type: 'warning',
            message: 'This Helm chart does not contain any template.',
          });
          cleanUrl();
        }
      } else {
        setTemplates(null);
        alertDispatcher.postAlert({
          type: 'warning',
          message: 'This Helm chart does not contain any template.',
        });
        cleanUrl();
      }
      setIsLoading(false);
    } catch (err: any) {
      if (err.kind === ErrorKind.NotFound) {
        alertDispatcher.postAlert({
          type: 'danger',
          message:
            'We could not find the templates for this chart version. Please check that the chart tgz package still exists in the source repository as it might not be available anymore.',
          dismissOn: 10 * 1000, // 10s
        });
      } else {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred getting chart templates, please try again later.',
        });
      }
      setTemplates(null);
      setValues(null);
      cleanUrl();
      setIsLoading(false);
    }
  }

  const onOpenModal = () => {
    if (templates && props.packageId === currentPkgId && props.version === currentVersion) {
      setOpenStatus(true);
      updateUrl({ template: templates[0].name });
    } else {
      getChartTemplates();
    }
  };

  const onCloseModal = () => {
    setOpenStatus(false);
    setEnabledDiff(false);
    setComparedVersion('');
    history.replace({
      search: '',
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  const isDisabledDiffView = props.sortedVersions.length <= 1;

  return (
    <div className="mb-2">
      <div className="text-center">
        <button
          className="btn btn-outline-secondary btn-sm text-nowrap w-100"
          onClick={onOpenModal}
          aria-label="Open templates modal"
        >
          <div className="d-flex flex-row align-items-center justify-content-center">
            {isLoading ? (
              <>
                <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                <span className="d-none d-md-inline ms-2 fw-bold">Loading templates...</span>
              </>
            ) : (
              <>
                <ImInsertTemplate />
                <span className="ms-2 fw-bold text-uppercase">Templates</span>
              </>
            )}
          </div>
        </button>
      </div>

      {openStatus && templates && (
        <Modal
          modalDialogClassName={styles.modalDialog}
          modalClassName="h-100"
          header={
            <div className="d-flex flex-row align-items-center flex-grow-1">
              <div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Templates</div>
              <div className="mx-4">
                <div className="btn-group" role="group">
                  <ElementWithTooltip
                    element={
                      <button
                        type="button"
                        className={classnames('btn btn-outline-primary btn-sm dropdown-toggle', {
                          disabled: isDisabledDiffView,
                        })}
                        onClick={() => setVisibleDropdown(!visibleDropdown)}
                        aria-label="Open diff template view"
                        aria-disabled={isDisabledDiffView}
                      >
                        <span className="pe-2">
                          {enabledDiff ? (
                            <span>
                              Comparing to version:
                              <span className="fw-bold ps-2">{comparedVersion}</span>
                            </span>
                          ) : (
                            'Compare to version ...'
                          )}
                        </span>
                      </button>
                    }
                    visibleTooltip={isDisabledDiffView}
                    tooltipMessage="There is only one version of this chart"
                    active
                  />

                  <div
                    ref={ref}
                    role="complementary"
                    className={classnames(
                      'dropdown-menu dropdown-menu-end text-nowrap overflow-hidden',
                      styles.dropdown,
                      {
                        show: visibleDropdown,
                      }
                    )}
                  >
                    {enabledDiff && (
                      <>
                        <button
                          type="button"
                          className="dropdown-item"
                          onClick={() => {
                            setVisibleDropdown(false);
                            if (versionsWrapper && versionsWrapper.current) {
                              versionsWrapper.current.scroll(0, 0);
                            }
                            setEnabledDiff(false);
                            setComparedVersion('');
                          }}
                        >
                          <div className="d-flex flex-row align-items-center">
                            <MdClose />
                            <div className="ms-2">Exit compare mode</div>
                          </div>
                        </button>

                        <div className="dropdown-divider mb-0" />
                      </>
                    )}

                    <div
                      ref={versionsWrapper}
                      className={classnames('overflow-scroll h-100', { [`pt-2 ${styles.versionsList}`]: enabledDiff })}
                    >
                      {props.sortedVersions.map((v: VersionData) => {
                        if (v.version === props.version) return null;
                        return (
                          <button
                            key={`opt_${v.version}`}
                            className={`dropdown-item ${styles.btnItem}`}
                            onClick={() => onVersionChange(v.version)}
                          >
                            <div className="d-flex flex-row align-items-center">
                              <div className={styles.itemIcon}>
                                {v.version === comparedVersion && <GoCheck className="text-success" />}
                              </div>
                              <div className="ms-2 text-truncate">{v.version}</div>
                            </div>
                          </button>
                        );
                      })}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          }
          onClose={onCloseModal}
          closeButton={
            <div className="w-100 d-flex flex-row align-items-center justify-content-between">
              <small className="me-3">
                {!enabledDiff && (
                  <>
                    <span className="fw-bold">TIP:</span> some extra info may be displayed when hovering over{' '}
                    <span className="fw-bold">values</span> entries and other{' '}
                    <span className="fw-bold">built-in objects and functions</span>.
                  </>
                )}
              </small>

              <button
                className="btn btn-sm btn-outline-secondary text-uppercase"
                onClick={() => setOpenStatus(false)}
                aria-label="Close"
              >
                <div className="d-flex flex-row align-items-center">
                  <MdClose className="me-2" />
                  <div>Close</div>
                </div>
              </button>
            </div>
          }
          open={openStatus}
          breakPoint="md"
          footerClassName={styles.modalFooter}
        >
          <div className="h-100 mw-100">
            {!isUndefined(templates) && (
              <>
                {enabledDiff ? (
                  <CompareView
                    packageId={props.packageId}
                    templates={templates}
                    currentVersion={props.version}
                    updateUrl={updateUrl}
                    comparedVersion={comparedVersion}
                    visibleTemplate={props.visibleTemplate}
                    formatTemplates={formatTemplates}
                  />
                ) : (
                  <TemplatesView
                    templates={templates}
                    values={values}
                    normalizedName={props.normalizedName}
                    visibleTemplate={props.visibleTemplate}
                    updateUrl={updateUrl}
                  />
                )}
              </>
            )}
          </div>
        </Modal>
      )}
    </div>
  );
}
Example #11
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
Values = (props: Props) => {
  const history = useHistory();
  const [openStatus, setOpenStatus] = useState<boolean>(false);
  const [values, setValues] = useState<string | undefined | null>();
  const [currentPkgId, setCurrentPkgId] = useState<string | undefined>(undefined);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [lines, setLines] = useState<Lines | undefined>();
  const [enabledDiff, setEnabledDiff] = useState<boolean>(!isUndefined(props.compareVersionTo));
  const [comparedVersion, setComparedVersion] = useState<string>(props.compareVersionTo || '');
  const [visibleDropdown, setVisibleDropdown] = useState<boolean>(false);
  const ref = useRef(null);
  const versionsWrapper = useRef<HTMLDivElement>(null);
  useOutsideClick([ref], visibleDropdown, () => setVisibleDropdown(false));

  const cleanUrl = () => {
    history.replace({
      search: '',
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  const updateUrl = (q: ValuesQuery) => {
    let selectedPath;
    if (!isUndefined(q.selectedLine) && !isUndefined(lines)) {
      selectedPath = lines[parseInt(q.selectedLine)];
    }
    history.replace({
      search: `?modal=values${selectedPath ? `&path=${selectedPath}` : ''}${
        q.template ? `&template=${q.template}` : ''
      }${q.compareTo ? `&compare-to=${q.compareTo}` : ''}`,
      state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
    });
  };

  async function getValues() {
    try {
      setIsLoading(true);
      const data = await API.getChartValues(props.packageId, props.version);
      setValues(data);
      setLines(getPathsPerLine(data));
      setCurrentPkgId(props.packageId);
      setIsLoading(false);
      setOpenStatus(true);
    } catch (err: any) {
      if (err.kind === ErrorKind.NotFound) {
        alertDispatcher.postAlert({
          type: 'danger',
          message:
            'We could not find the default values for this chart version. Please check that the chart tgz package still exists in the source repository as it might not be available anymore.',
          dismissOn: 10 * 1000, // 10s
        });
      } else {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred getting chart default values, please try again later.',
        });
      }
      setValues(null);
      setIsLoading(false);
      cleanUrl();
    }
  }

  const onOpenModal = () => {
    getValues();
    if (!props.visibleValues) {
      updateUrl({});
    }
  };

  const onCloseModal = () => {
    setValues(undefined);
    setOpenStatus(false);
    setEnabledDiff(false);
    setComparedVersion('');
    cleanUrl();
  };

  const onVersionChange = (version: string) => {
    setComparedVersion(version);
    updateUrl({ compareTo: version });
    setVisibleDropdown(false);
    setEnabledDiff(true);
  };

  useEffect(() => {
    if (props.visibleValues && !openStatus && isUndefined(currentPkgId)) {
      onOpenModal();
    }
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if ((openStatus || props.visibleValues) && !isUndefined(currentPkgId)) {
      onCloseModal();
    }
  }, [props.packageId]); /* eslint-disable-line react-hooks/exhaustive-deps */

  const isDisabledDiffView = props.sortedVersions.length <= 1;

  return (
    <>
      <button
        className="btn btn-outline-secondary btn-sm w-100"
        onClick={onOpenModal}
        aria-label="Open default values modal"
      >
        <div className="d-flex flex-row align-items-center justify-content-center text-uppercase">
          {isLoading ? (
            <>
              <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
              <span className="ms-2 fw-bold">Getting values...</span>
            </>
          ) : (
            <>
              <VscListTree className="me-2" />
              <span className="fw-bold">Default values</span>
            </>
          )}
        </div>
      </button>

      {openStatus && values && (
        <Modal
          modalDialogClassName={styles.modalDialog}
          modalClassName="h-100"
          header={
            <div className="d-flex flex-row align-items-center flex-grow-1">
              <div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Default values</div>
              <div className="mx-4">
                <div className="btn-group" role="group">
                  <ElementWithTooltip
                    element={
                      <button
                        type="button"
                        className={classnames('btn btn-outline-primary btn-sm dropdown-toggle', {
                          disabled: isDisabledDiffView,
                        })}
                        onClick={() => setVisibleDropdown(!visibleDropdown)}
                        aria-label="Open diff template view"
                        aria-disabled={isDisabledDiffView}
                      >
                        <span className="pe-2">
                          {enabledDiff ? (
                            <span>
                              Comparing to version:
                              <span className="fw-bold ps-2">{comparedVersion}</span>
                            </span>
                          ) : (
                            'Compare to version ...'
                          )}
                        </span>
                      </button>
                    }
                    visibleTooltip={isDisabledDiffView}
                    tooltipMessage="There is only one version of this chart"
                    active
                  />

                  <div
                    ref={ref}
                    role="complementary"
                    className={classnames(
                      'dropdown-menu dropdown-menu-end text-nowrap overflow-hidden',
                      styles.versionsDropdown,
                      {
                        show: visibleDropdown,
                      }
                    )}
                  >
                    {enabledDiff && (
                      <>
                        <button
                          type="button"
                          className="dropdown-item"
                          onClick={() => {
                            setVisibleDropdown(false);
                            if (versionsWrapper && versionsWrapper.current) {
                              versionsWrapper.current.scroll(0, 0);
                            }
                            setEnabledDiff(false);
                            setComparedVersion('');
                          }}
                        >
                          <div className="d-flex flex-row align-items-center">
                            <MdClose />
                            <div className="ms-2">Exit compare mode</div>
                          </div>
                        </button>

                        <div className="dropdown-divider mb-0" />
                      </>
                    )}

                    <div
                      ref={versionsWrapper}
                      className={classnames('overflow-scroll h-100', { [`pt-2 ${styles.versionsList}`]: enabledDiff })}
                    >
                      {props.sortedVersions.map((v: VersionData) => {
                        if (v.version === props.version) return null;
                        return (
                          <button
                            key={`opt_${v.version}`}
                            className={`dropdown-item ${styles.btnItem}`}
                            onClick={() => onVersionChange(v.version)}
                          >
                            <div className="d-flex flex-row align-items-center">
                              <div className={styles.itemIcon}>
                                {v.version === comparedVersion && <GoCheck className="text-success" />}
                              </div>
                              <div className="ms-2 text-truncate">{v.version}</div>
                            </div>
                          </button>
                        );
                      })}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          }
          onClose={onCloseModal}
          open={openStatus}
          breakPoint="md"
          footerClassName={styles.modalFooter}
        >
          <div className="mw-100 h-100 d-flex flex-column overflow-hidden">
            {values && (
              <>
                {enabledDiff ? (
                  <CompareView
                    packageId={props.packageId}
                    values={values}
                    currentVersion={props.version}
                    comparedVersion={comparedVersion}
                  />
                ) : (
                  <ValuesView
                    values={values}
                    lines={lines}
                    normalizedName={props.normalizedName}
                    updateUrl={updateUrl}
                    visibleValuesPath={props.visibleValuesPath}
                    searchUrlReferer={props.searchUrlReferer}
                    fromStarredPage={props.fromStarredPage}
                  />
                )}
              </>
            )}
          </div>
        </Modal>
      )}
    </>
  );
}
Example #12
Source File: ModalSolutionPass.tsx    From FaztCommunityMatch with MIT License 4 votes vote down vote up
function ModalSolutionPass({ showModal, setShowModal }) {
    const [formState, setFormState] = useState<FormStateValues>(formInitialValues)
    const [formErrors, setFormErrors] = useState<FormErrors>(formInitialErrors)
    const isEmpty = (value: string): boolean => {
        return value.trim().length === 0
    }

    const handleOnChange = (
        e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
    ): void => {
        console.log(e.target.name)
        const hasError = isEmpty(e.target.value)
        setFormState({ ...formState, [e.target.name]: e.target.value })
        setFormErrors({ ...formErrors, [e.target.name]: hasError })
    }

    const modalRef = useRef()

    const closeModal = e => {
        if (modalRef.current === e.target) {
            setShowModal(false)
        }
    };

    const keyPress = useCallback(e => {
        if (e.key === 'Escape' && showModal) {
            setShowModal(false)
        }
    }, [setShowModal, showModal])

    useEffect(() => {
        document.addEventListener('keydown', keyPress);
        return () => document.removeEventListener('keydown', keyPress);
    },
        [keyPress]
    )

    const CloseModalButton = styled(MdClose)`
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
padding: 0;
z-index: 4;
`

    return (
        <>

            {showModal
                ? (
                    <section className="section-modal-solution"
                        ref={modalRef}
                        onClick={closeModal}>
                        <div className="content-modal-solution">
                            <h1 className="title-modal-solution">Ingresa tu correo para recuperar la contraseƱa</h1>
                            <EmailInput
                                className="margin-input-email"
                                required
                                placeHolder="*Ingresa tu Correo"
                                handleOnChange={handleOnChange}
                                error={formErrors.email}
                                name="email"
                                value={formState.email}
                            />
                            <div className="content-btn-send-mondal-solution">
                                <a className="btn-medium-send-modal-solution" href="">
                                    Enviar
                                </a>
                            </div>
                            <CloseModalButton
                                aria-label='Close modal'
                                onClick={() => setShowModal(prev => !prev)} />
                        </div>
                    </section>
                )

                : null}

        </>
    )
}