react-icons/ri#RiArrowLeftRightLine TypeScript Examples

The following examples show how to use react-icons/ri#RiArrowLeftRightLine. 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: Card.tsx    From hub with Apache License 2.0 4 votes vote down vote up
RepositoryCard = (props: Props) => {
  const history = useHistory();
  const { ctx } = useContext(AppCtx);
  const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
  const [transferModalStatus, setTransferModalStatus] = useState<boolean>(false);
  const [deletionModalStatus, setDeletionModalStatus] = useState<boolean>(false);
  const [badgeModalStatus, setBadgeModalStatus] = useState<boolean>(false);
  const dropdownMenu = useRef(null);
  const organizationName = ctx.prefs.controlPanel.selectedOrg;
  const hasErrors = !isUndefined(props.repository.lastTrackingErrors) && !isNull(props.repository.lastTrackingErrors);
  const hasScanningErrors =
    !isUndefined(props.repository.lastScanningErrors) && !isNull(props.repository.lastScanningErrors);
  const [openErrorsModal, setOpenErrorsModal] = useState<boolean>(false);
  const [openScanningErrorsModal, setOpenScanningErrorsModal] = useState<boolean>(false);

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

  useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          <div className="mt-3 m-md-0 d-flex flex-row d-md-none">
            <OfficialBadge official={props.repository.official} className="me-3" type="repo" />
            <VerifiedPublisherBadge verifiedPublisher={props.repository.verifiedPublisher} className="me-3" />
            <DisabledRepositoryBadge disabled={props.repository.disabled!} />
          </div>
        </div>
      </div>
    </div>
  );
}
Example #2
Source File: ClaimOwnershipModal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
ClaimRepositoryOwnerShipModal = (props: Props) => {
  const { ctx } = useContext(AppCtx);
  const siteName = getMetaTag('siteName');
  const form = useRef<HTMLFormElement>(null);
  const [isFetchingOrgs, setIsFetchingOrgs] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [isValidated, setIsValidated] = useState(false);
  const [apiError, setApiError] = useState<string | null>(null);
  const [apiOrgsError, setApiOrgsError] = useState<string | null>(null);
  const [apiReposError, setApiReposError] = useState<string | null>(null);
  const organizationName = ctx.prefs.controlPanel.selectedOrg;
  const [selectedClaimOption, setSelectedClaimOption] = useState<'org' | 'user'>(
    !isUndefined(organizationName) ? 'org' : 'user'
  );
  const [claimingOrg, setClaimingOrg] = useState<string>(organizationName || '');
  const [organizations, setOrganizations] = useState<Organization[] | undefined>(undefined);
  const [repoItem, setRepoItem] = useState<Repository | null>(null);

  const handleOrgChange = (event: ChangeEvent<HTMLSelectElement>) => {
    setClaimingOrg(event.target.value);
    setSelectedClaimOption(event.target.value === '' ? 'user' : 'org');
  };

  const handleClaimingFromOpt = (type: 'user' | 'org') => {
    if (type === 'user') {
      setClaimingOrg('');
    }
    setSelectedClaimOption(type);
  };

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

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

  const onRepoSelect = (repo: Repository): void => {
    setRepoItem(repo);
  };

  async function claimRepository() {
    try {
      await API.claimRepositoryOwnership(repoItem!, claimingOrg || undefined);
      if (!isUndefined(props.onSuccess)) {
        props.onSuccess();
      }
      setIsSending(false);
      onCloseModal();
    } catch (err: any) {
      setIsSending(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        let error = compoundErrorMessage(err, 'An error occurred claiming the repository');
        if (err.kind === ErrorKind.Forbidden) {
          error =
            'You do not have permissions to claim this repository ownership. Please make sure your metadata file has been setup correctly.';
        }
        setApiError(error);
      } else {
        props.onAuthError();
      }
    }
  }

  const submitForm = () => {
    cleanApiError();
    setIsSending(true);
    if (form.current && validateForm(form.current)) {
      claimRepository();
    } else {
      setIsSending(false);
    }
  };

  const validateForm = (form: HTMLFormElement): boolean => {
    setIsValidated(true);
    return form.checkValidity();
  };

  const getOrgsNames = (): string[] => {
    if (organizations) {
      return organizations.map((org: Organization) => org.name);
    }
    return [];
  };

  useEffect(() => {
    async function fetchOrganizations() {
      try {
        setIsFetchingOrgs(true);
        const orgs = await API.getAllUserOrganizations();
        const confirmedOrganizations = orgs.filter((org: Organization) => org.confirmed);
        setOrganizations(confirmedOrganizations);
        setApiOrgsError(null);
        setIsFetchingOrgs(false);
      } catch (err: any) {
        setIsFetchingOrgs(false);
        if (err.kind !== ErrorKind.Unauthorized) {
          setOrganizations([]);
          setApiOrgsError('An error occurred getting your organizations, please try again later.');
        } else {
          props.onAuthError();
        }
      }
    }

    fetchOrganizations();
  }, [organizationName, props]);

  const getPublisher = (repo: Repository) => (
    <small className="ms-0 ms-sm-2">
      <span className="d-none d-sm-inline">(</span>
      <small className={`d-none d-md-inline text-muted me-1 text-uppercase ${styles.legend}`}>Publisher: </small>
      <div className={`d-inline me-1 ${styles.tinyIcon}`}>{repo.userAlias ? <FaUser /> : <MdBusiness />}</div>
      <span>{repo.userAlias || repo.organizationDisplayName || repo.organizationName}</span>
      <span className="d-none d-sm-inline">)</span>
    </small>
  );

  return (
    <Modal
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Claim repository ownership</div>}
      open={props.open}
      modalClassName={styles.modal}
      size="xl"
      closeButton={
        <button
          className="btn btn-sm btn-outline-secondary"
          type="button"
          disabled={isSending || isNull(repoItem)}
          onClick={submitForm}
          aria-label="Claim ownership"
        >
          {isSending ? (
            <>
              <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
              <span className="ms-2">Claiming ownership...</span>
            </>
          ) : (
            <div className="text-uppercase d-flex flex-row align-items-center">
              <RiArrowLeftRightLine className="me-2" />
              <div>Claim ownership</div>
            </div>
          )}
        </button>
      }
      onClose={onCloseModal}
      error={apiOrgsError || apiReposError || apiError}
      cleanError={cleanApiError}
      noScrollable
    >
      <div className="w-100">
        <div className="mt-4">
          <p>
            Before claiming a repository ownership, we need to verify that you actually own it. To prove that, you need
            to add a{' '}
            <ExternalLink
              href="https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml"
              className="text-primary fw-bold"
              label="Open documentation"
            >
              metadata file
            </ExternalLink>{' '}
            to your repository and include yourself (or the person who will do the request) as an owner. This will be
            checked during the ownership claim process. Please make sure the email used in the metadata file matches
            with the one you use in {siteName}.
          </p>
        </div>
        <form
          data-testid="claimRepoForm"
          ref={form}
          className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
          onFocus={cleanApiError}
          autoComplete="on"
          noValidate
        >
          <div>
            <div className="d-flex flex-column my-3">
              <label className={`form-label fw-bold ${styles.label}`} htmlFor="description">
                Repository:
              </label>

              {!isNull(repoItem) ? (
                <div
                  data-testid="activeRepoItem"
                  className={`border border-secondary w-100 rounded mt-1 ${styles.repoWrapper}`}
                >
                  <div className="d-flex flex-row flex-nowrap align-items-stretch justify-content-between">
                    <div className="flex-grow-1 text-truncate py-2">
                      <div className="d-flex flex-row align-items-center h-100 text-truncate">
                        <div className="d-none d-md-inline">
                          <RepositoryIcon kind={repoItem.kind} className={`mx-3 w-auto ${styles.icon}`} />
                        </div>

                        <div className="ms-2 fw-bold mb-0 text-truncate text-muted">
                          <span className="text-dark">{repoItem.name}</span>{' '}
                          <small className="text-muted">({repoItem.url})</small>
                          <span className={`d-inline d-sm-none ${styles.legend}`}>
                            <span className="mx-2">/</span>
                            {getPublisher(repoItem)}
                          </span>
                        </div>

                        <div className="px-2 ms-auto w-50 text-dark text-truncate d-none d-sm-inline">
                          {getPublisher(repoItem)}
                        </div>
                      </div>
                    </div>

                    <div>
                      <button
                        className={`btn btn-close btn-sm h-100 rounded-0 border-start px-3 py-0 ${styles.closeButton}`}
                        onClick={() => setRepoItem(null)}
                        aria-label="Close"
                      ></button>
                    </div>
                  </div>
                </div>
              ) : (
                <div className={`mt-2 ${styles.searchWrapper}`}>
                  <SearchRepositories
                    label="claim-repo-ownership"
                    disabledRepositories={{
                      users: ctx.user ? [ctx.user.alias] : [],
                      organizations: getOrgsNames(),
                    }}
                    onSelection={onRepoSelect}
                    onAuthError={props.onAuthError}
                    visibleUrl
                  />
                </div>
              )}
            </div>

            <label id="claiming" className={`form-label fw-bold ${styles.label}`}>
              Transfer to:
            </label>
            <div className="form-check mb-2">
              <input
                aria-labelledby="claiming user"
                className="form-check-input"
                type="radio"
                name="claim"
                id="user"
                value="user"
                checked={selectedClaimOption === 'user'}
                onChange={() => handleClaimingFromOpt('user')}
                required
              />
              <label id="user" className={`form-check-label ${styles.label}`} htmlFor="user">
                My user
              </label>
            </div>

            <div className="form-check mb-3">
              <input
                aria-labelledby="claiming org"
                className="form-check-input"
                type="radio"
                name="claim"
                id="org"
                value="org"
                checked={selectedClaimOption === 'org'}
                onChange={() => handleClaimingFromOpt('org')}
                required
              />
              <label id="org" className={`form-check-label ${styles.label}`} htmlFor="org">
                Organization
              </label>
            </div>
          </div>

          <div className="d-flex flex-row align-items-center position-relative mb-3">
            <div className=" w-75 mb-2">
              <select
                className="form-select"
                aria-label="org-select"
                value={claimingOrg}
                onChange={handleOrgChange}
                required={selectedClaimOption === 'org'}
              >
                {!isUndefined(organizations) && (
                  <>
                    <option value="">Select organization</option>
                    {organizations.map((org: Organization) => (
                      <option key={`opt_${org.name}`} value={org.name}>
                        {org.name}
                      </option>
                    ))}
                  </>
                )}
              </select>
              <div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
            </div>
            {isFetchingOrgs && (
              <div className="d-inline ms-3">
                <span className="spinner-border spinner-border-sm text-primary" />
              </div>
            )}
          </div>

          <small className="text-muted text-break mt-3">
            <p>It may take a few minutes for this change to be visible across the Hub.</p>
          </small>
        </form>
      </div>
    </Modal>
  );
}
Example #3
Source File: TransferModal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
TransferRepositoryModal = (props: Props) => {
  const { ctx } = useContext(AppCtx);
  const form = useRef<HTMLFormElement>(null);
  const [isFetchingOrgs, setIsFetchingOrgs] = useState(false);
  const [isSending, setIsSending] = useState(false);
  const [isValidated, setIsValidated] = useState(false);
  const [apiError, setApiError] = useState<string | null>(null);
  const organizationName = ctx.prefs.controlPanel.selectedOrg;
  const [selectedTransferOption, setSelectedTransferOption] = useState<'org' | 'user'>(
    isUndefined(organizationName) ? 'org' : 'user'
  );
  const [orgToTransfer, setOrgToTransfer] = useState<string | undefined>(undefined);
  const [organizations, setOrganizations] = useState<Organization[] | undefined>(undefined);

  const handleOrgChange = (event: ChangeEvent<HTMLSelectElement>) => {
    setOrgToTransfer(event.target.value || undefined);
  };

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

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

  async function transferRepository() {
    try {
      await API.transferRepository({
        repositoryName: props.repository.name,
        toOrgName: orgToTransfer,
        fromOrgName: organizationName,
      });
      if (!isUndefined(props.onSuccess)) {
        props.onSuccess();
      }
      setIsSending(false);
      onCloseModal();
    } catch (err: any) {
      setIsSending(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        let error = compoundErrorMessage(err, 'An error occurred transferring the repository');
        if (!isUndefined(organizationName) && err.kind === ErrorKind.Forbidden) {
          error = 'You do not have permissions to transfer a repository to the organization.';
        }
        setApiError(error);
      } else {
        props.onAuthError();
      }
    }
  }

  const submitForm = () => {
    cleanApiError();
    setIsSending(true);
    if (form.current && validateForm(form.current)) {
      transferRepository();
    } else {
      setIsSending(false);
    }
  };

  const validateForm = (form: HTMLFormElement): boolean => {
    setIsValidated(true);
    return form.checkValidity();
  };

  useEffect(() => {
    async function fetchOrganizations() {
      try {
        setIsFetchingOrgs(true);
        let orgs = await API.getAllUserOrganizations();
        if (!isUndefined(organizationName)) {
          orgs = orgs.filter((org: Organization) => org.name !== organizationName);
        }
        setOrganizations(orgs);
        setApiError(null);
        setIsFetchingOrgs(false);
      } catch (err: any) {
        setIsFetchingOrgs(false);
        if (err.kind !== ErrorKind.Unauthorized) {
          setOrganizations([]);
          setApiError('An error occurred getting your organizations, please try again later.');
        } else {
          props.onAuthError();
        }
      }
    }

    fetchOrganizations();
  }, [organizationName, props]);

  return (
    <Modal
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Transfer repository</div>}
      open={props.open}
      modalClassName={styles.modal}
      closeButton={
        <button
          className="btn btn-sm btn-outline-secondary"
          type="button"
          disabled={isSending}
          onClick={submitForm}
          aria-label="Transfer repository"
        >
          {isSending ? (
            <>
              <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
              <span className="ms-2">Transferring repository...</span>
            </>
          ) : (
            <div className="d-flex flex-row align-items-center text-uppercase">
              <RiArrowLeftRightLine className="me-2" />
              <span>Transfer</span>
            </div>
          )}
        </button>
      }
      onClose={onCloseModal}
      error={apiError}
      cleanError={cleanApiError}
    >
      <div className="w-100">
        <form
          data-testid="transferRepoForm"
          ref={form}
          className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
          onFocus={cleanApiError}
          autoComplete="on"
          noValidate
        >
          {!isUndefined(organizationName) ? (
            <>
              <div className="form-check mb-3">
                <input
                  className="form-check-input"
                  type="radio"
                  name="transfer"
                  id="user"
                  value="user"
                  checked={selectedTransferOption === 'user'}
                  onChange={() => setSelectedTransferOption('user')}
                  required
                />
                <label className={`form-check-label fw-bold ${styles.label}`} htmlFor="user">
                  Transfer to my user
                </label>
              </div>

              <div className="form-check mb-3">
                <input
                  className="form-check-input"
                  type="radio"
                  name="transfer"
                  id="org"
                  value="org"
                  checked={selectedTransferOption === 'org'}
                  onChange={() => setSelectedTransferOption('org')}
                  required
                />
                <label className={`form-check-label fw-bold ${styles.label}`} htmlFor="org">
                  Transfer to organization
                </label>
              </div>
            </>
          ) : (
            <label className={`form-label fw-bold ${styles.label}`}>Transfer to organization</label>
          )}

          <div
            data-testid="selectOrgsWrapper"
            className={classnames('d-flex flex-row align-items-center position-relative mb-3', {
              invisible: selectedTransferOption === 'user',
            })}
          >
            <div className=" w-75 mb-2">
              <select
                className="form-select"
                aria-label="org-select"
                value={orgToTransfer}
                onChange={handleOrgChange}
                required={selectedTransferOption === 'org'}
              >
                {!isUndefined(organizations) && (
                  <>
                    <option value="">Select organization</option>
                    {organizations.map((org: Organization) => (
                      <option key={`opt_${org.name}`} value={org.name}>
                        {org.name}
                      </option>
                    ))}
                  </>
                )}
              </select>
              <div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
            </div>
            {isFetchingOrgs && (
              <div className="d-inline ms-3" role="status">
                <span className="spinner-border spinner-border-sm text-primary" />
              </div>
            )}
          </div>

          <small className="text-muted text-break mt-3">
            <p>It may take a few minutes for this change to be visible across the Hub.</p>
          </small>
        </form>
      </div>
    </Modal>
  );
}
Example #4
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
RepositoriesSection = (props: Props) => {
  const history = useHistory();
  const { ctx, dispatch } = useContext(AppCtx);
  const [isLoading, setIsLoading] = useState(false);
  const [modalStatus, setModalStatus] = useState<ModalStatus>({
    open: false,
  });
  const [openClaimRepo, setOpenClaimRepo] = useState<boolean>(false);
  const [repositories, setRepositories] = useState<Repo[] | undefined>(undefined);
  const [activeOrg, setActiveOrg] = useState<undefined | string>(ctx.prefs.controlPanel.selectedOrg);
  const [apiError, setApiError] = useState<null | string>(null);
  const [activePage, setActivePage] = useState<number>(props.activePage ? parseInt(props.activePage) : 1);

  const calculateOffset = (pageNumber?: number): number => {
    return DEFAULT_LIMIT * ((pageNumber || activePage) - 1);
  };

  const [offset, setOffset] = useState<number>(calculateOffset());
  const [total, setTotal] = useState<number | undefined>(undefined);

  const onPageNumberChange = (pageNumber: number): void => {
    setOffset(calculateOffset(pageNumber));
    setActivePage(pageNumber);
  };

  const updatePageNumber = () => {
    history.replace({
      search: `?page=${activePage}${props.repoName ? `&repo-name=${props.repoName}` : ''}${
        props.visibleModal ? `&modal=${props.visibleModal}` : ''
      }`,
    });
  };

  async function fetchRepositories() {
    try {
      setIsLoading(true);
      let filters: { [key: string]: string[] } = {};
      if (activeOrg) {
        filters.org = [activeOrg];
      } else {
        filters.user = [ctx.user!.alias];
      }
      let query: SearchQuery = {
        offset: offset,
        limit: DEFAULT_LIMIT,
        filters: filters,
      };
      const data = await API.searchRepositories(query);
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        const repos = data.items;
        setRepositories(repos);
        setTotal(total);
        // Check if active repo logs modal is in the available repos
        if (!isUndefined(props.repoName)) {
          const activeRepo = repos.find((repo: Repo) => repo.name === props.repoName);
          // Clean query string if repo is not available
          if (isUndefined(activeRepo)) {
            dispatch(unselectOrg());
            history.replace({
              search: `?page=${activePage}`,
            });
          }
        } else {
          updatePageNumber();
        }
      }
      setApiError(null);
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setApiError('An error occurred getting the repositories, please try again later.');
        setRepositories([]);
      } else {
        props.onAuthError();
      }
    }
  }

  useEffect(() => {
    if (isUndefined(props.activePage)) {
      updatePageNumber();
    }
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (props.activePage && activePage !== parseInt(props.activePage)) {
      fetchRepositories();
    }
  }, [activePage]); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (!isUndefined(repositories)) {
      if (activePage === 1) {
        // fetchRepositories is forced when context changes
        fetchRepositories();
      } else {
        // when current page is different to 1, to update page number fetchRepositories is called
        onPageNumberChange(1);
      }
    }
  }, [activeOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (activeOrg !== ctx.prefs.controlPanel.selectedOrg) {
      setActiveOrg(ctx.prefs.controlPanel.selectedOrg);
    }
  }, [ctx.prefs.controlPanel.selectedOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */

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

  return (
    <main
      role="main"
      className="pe-xs-0 pe-sm-3 pe-md-0 d-flex flex-column flex-md-row justify-content-between my-md-4"
    >
      <div className="flex-grow-1 w-100">
        <div>
          <div className="d-flex flex-row align-items-center justify-content-between pb-2 border-bottom">
            <div className={`h3 pb-0 ${styles.title}`}>Repositories</div>

            <div>
              <button
                className={`btn btn-outline-secondary btn-sm text-uppercase me-0 me-md-2 ${styles.btnAction}`}
                onClick={fetchRepositories}
                aria-label="Refresh repositories list"
              >
                <div className="d-flex flex-row align-items-center justify-content-center">
                  <IoMdRefresh className="d-inline d-md-none" />
                  <IoMdRefreshCircle className="d-none d-md-inline me-2" />
                  <span className="d-none d-md-inline">Refresh</span>
                </div>
              </button>

              <button
                className={`btn btn-outline-secondary btn-sm text-uppercase me-0 me-md-2 ${styles.btnAction}`}
                onClick={() => setOpenClaimRepo(true)}
                aria-label="Open claim repository modal"
              >
                <div className="d-flex flex-row align-items-center justify-content-center">
                  <RiArrowLeftRightLine className="me-md-2" />
                  <span className="d-none d-md-inline">Claim ownership</span>
                </div>
              </button>

              <ActionBtn
                className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
                contentClassName="justify-content-center"
                onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                  e.preventDefault();
                  setModalStatus({ open: true });
                }}
                action={AuthorizerAction.AddOrganizationRepository}
                label="Open add repository modal"
              >
                <>
                  <MdAdd className="d-inline d-md-none" />
                  <MdAddCircle className="d-none d-md-inline me-2" />
                  <span className="d-none d-md-inline">Add</span>
                </>
              </ActionBtn>
            </div>
          </div>
        </div>

        {modalStatus.open && (
          <RepositoryModal
            open={modalStatus.open}
            repository={modalStatus.repository}
            onSuccess={fetchRepositories}
            onAuthError={props.onAuthError}
            onClose={() => setModalStatus({ open: false })}
          />
        )}

        {openClaimRepo && (
          <ClaimOwnershipRepositoryModal
            open={openClaimRepo}
            onAuthError={props.onAuthError}
            onClose={() => setOpenClaimRepo(false)}
            onSuccess={fetchRepositories}
          />
        )}

        {(isLoading || isUndefined(repositories)) && <Loading />}

        <p className="mt-5">
          If you want your repositories to be labeled as <span className="fw-bold">Verified Publisher</span>, you can
          add a{' '}
          <ExternalLink
            href="https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml"
            className="text-reset"
            label="Open documentation"
          >
            <u>metadata file</u>
          </ExternalLink>{' '}
          to each of them including the repository ID provided below. This label will let users know that you own or
          have control over the repository. The repository metadata file must be located at the path used in the
          repository URL.
        </p>

        {!isUndefined(repositories) && (
          <>
            {repositories.length === 0 ? (
              <NoData issuesLinkVisible={!isNull(apiError)}>
                {isNull(apiError) ? (
                  <>
                    <p className="h6 my-4">Add your first repository!</p>

                    <ActionBtn
                      className="btn btn-sm btn-outline-secondary"
                      onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                        e.preventDefault();
                        setModalStatus({ open: true });
                      }}
                      action={AuthorizerAction.AddOrganizationRepository}
                      label="Open add first repository modal"
                    >
                      <div className="d-flex flex-row align-items-center text-uppercase">
                        <MdAddCircle className="me-2" />
                        <span>Add repository</span>
                      </div>
                    </ActionBtn>
                  </>
                ) : (
                  <>{apiError}</>
                )}
              </NoData>
            ) : (
              <>
                <div className="row mt-3 mt-md-4 gx-0 gx-xxl-4" data-testid="repoList">
                  {repositories.map((repo: Repo) => (
                    <RepositoryCard
                      key={`repo_${repo.name}`}
                      repository={repo}
                      visibleModal={
                        // Legacy - old tracking errors email were not passing modal param
                        !isUndefined(props.repoName) && repo.name === props.repoName
                          ? props.visibleModal || 'tracking'
                          : undefined
                      }
                      setModalStatus={setModalStatus}
                      onSuccess={fetchRepositories}
                      onAuthError={props.onAuthError}
                    />
                  ))}
                </div>
                {!isUndefined(total) && (
                  <Pagination
                    limit={DEFAULT_LIMIT}
                    offset={offset}
                    total={total}
                    active={activePage}
                    className="my-5"
                    onChange={onPageNumberChange}
                  />
                )}
              </>
            )}
          </>
        )}
      </div>
    </main>
  );
}