react-icons/md#MdBusiness TypeScript Examples

The following examples show how to use react-icons/md#MdBusiness. 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: ProductionBadge.tsx    From hub with Apache License 2.0 6 votes vote down vote up
ProductionBadge = (props: Props) => {
  if (isUndefined(props.productionOrganizationsCount) || props.productionOrganizationsCount === 0) return null;
  return (
    <ElementWithTooltip
      className={props.className}
      element={<Label text="Production users" icon={<MdBusiness />} iconLegend={props.productionOrganizationsCount} />}
      tooltipMessage={`${props.productionOrganizationsCount} ${
        props.productionOrganizationsCount === 1 ? 'organization is' : 'organizations are'
      } using this package in production`}
      active
      visibleTooltip
    />
  );
}
Example #2
Source File: Stats.tsx    From hub with Apache License 2.0 6 votes vote down vote up
Stats = (props: Props) => {
  if (isUndefined(props.packageStats) && isUndefined(props.productionOrganizationsCount)) return null;

  return (
    <div className="d-flex flex-row flex-wrap align-items-baseline">
      {!isUndefined(props.packageStats) && (
        <>
          {props.packageStats.subscriptions > 0 && (
            <div data-testid="subscriptions" className="d-flex flex-row align-items-baseline me-3 mt-2">
              <FaUser className={styles.icon} />
              <small className="text-muted text-uppercase mx-1">Subscriptions:</small>
              <span className="fw-bold">{prettifyNumber(props.packageStats.subscriptions)}</span>
            </div>
          )}

          {props.packageStats.webhooks > 0 && (
            <div data-testid="webhooks" className="d-flex flex-row align-items-baseline me-3 mt-2">
              <GrConnect className={styles.icon} />
              <small className="text-muted text-uppercase mx-1">Webhooks:</small>
              <span className="fw-bold">{prettifyNumber(props.packageStats.webhooks)}</span>
            </div>
          )}
        </>
      )}

      {!isUndefined(props.productionOrganizationsCount) && props.productionOrganizationsCount > 0 && (
        <div data-testid="productionUsers" className="d-none d-md-flex flex-row align-items-baseline mt-2">
          <MdBusiness className={styles.icon} />
          <small className="text-muted text-uppercase mx-1">Production users:</small>
          <span className="fw-bold">{prettifyNumber(props.productionOrganizationsCount)}</span>
        </div>
      )}
    </div>
  );
}
Example #3
Source File: data.tsx    From hub with Apache License 2.0 5 votes vote down vote up
CONTROL_PANEL_SECTIONS: NavSection = {
  user: [
    {
      name: 'repositories',
      displayName: 'Repositories',
      disabled: false,
      icon: <GoPackage />,
    },
    {
      name: 'organizations',
      displayName: 'Organizations',
      disabled: false,
      icon: <MdBusiness />,
    },
    {
      name: 'settings',
      displayName: 'Settings',
      disabled: false,
      icon: <MdSettings />,
      subsections: [
        { displayName: 'Profile', name: 'profile', icon: <MdBusiness />, disabled: false },
        { displayName: 'Subscriptions', name: 'subscriptions', icon: <MdNotificationsActive />, disabled: false },
        { displayName: 'Webhooks', name: 'webhooks', icon: <GrConnect />, disabled: false },
        { displayName: 'API keys', name: 'api-keys', icon: <FaKey />, disabled: false },
      ],
    },
  ],
  org: [
    {
      name: 'repositories',
      displayName: 'Repositories',
      disabled: false,
      icon: <GoPackage />,
    },
    {
      name: 'members',
      displayName: 'Members',
      disabled: false,
      icon: <FaUserFriends />,
    },
    {
      name: 'settings',
      displayName: 'Settings',
      disabled: false,
      icon: <MdSettings />,
      subsections: [
        { displayName: 'Profile', name: 'profile', icon: <MdBusiness />, disabled: false },
        { displayName: 'Webhooks', name: 'webhooks', icon: <GrConnect />, disabled: false },
        {
          displayName: 'Authorization',
          name: 'authorization',
          icon: <FaScroll />,
          disabled: false,
          onlyDesktop: true,
        },
      ],
    },
  ],
}
Example #4
Source File: OrganizationInfo.tsx    From hub with Apache License 2.0 4 votes vote down vote up
OrganizationInfo = (props: Props) => {
  const history = useHistory();
  const ref = useRef(null);
  const [organization, setOrganization] = useState<Organization | null | undefined>(undefined);
  const [openStatus, setOpenStatus] = useState(false);
  const [onLinkHover, setOnLinkHover] = useState(false);
  const [onDropdownHover, setOnDropdownHover] = useState(false);
  const [fetchTimeout, setFetchTimeout] = useState<NodeJS.Timeout | null>(null);
  useOutsideClick([ref], openStatus, () => setOpenStatus(false));

  async function fetchOrganization() {
    try {
      setOrganization(await API.getOrganization(props.organizationName));
    } catch (err: any) {
      setOrganization(null);
    }
  }

  const openOrgInfo = () => {
    if (isUndefined(organization)) {
      setFetchTimeout(
        setTimeout(() => {
          fetchOrganization();
        }, FETCH_DELAY)
      );
    }
  };

  const cleanFetchTimeout = () => {
    if (fetchTimeout) {
      clearTimeout(fetchTimeout);
    }
  };

  useEffect(() => {
    let timeout: NodeJS.Timeout;
    if (organization && !openStatus && (onLinkHover || onDropdownHover)) {
      timeout = setTimeout(() => {
        setOpenStatus(true);
      }, 100);
    }
    if (openStatus && !onLinkHover && !onDropdownHover) {
      timeout = setTimeout(() => {
        // Delay to hide the dropdown to avoid hide it if user changes from link to dropdown
        setOpenStatus(false);
      }, 50);
    }

    return () => {
      if (!isUndefined(timeout)) {
        clearTimeout(timeout);
      }
      cleanFetchTimeout();
    };
  }, [onLinkHover, onDropdownHover, organization, openStatus]); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className={props.className}>
      <div className="position-absolute">
        <div
          ref={ref}
          role="complementary"
          className={classnames('dropdown-menu dropdown-menu-left text-wrap', styles.dropdown, {
            show: openStatus,
          })}
          onMouseEnter={() => setOnDropdownHover(true)}
          onMouseLeave={() => setOnDropdownHover(false)}
        >
          {organization && (
            <div className={styles.content}>
              <div className="d-flex flex-row align-items-center">
                <div
                  className={`d-flex align-items-center justify-content-center overflow-hidden me-2 p-1 position-relative border border-2 rounded-circle bg-white ${styles.imageWrapper} imageWrapper`}
                >
                  {organization.logoImageId ? (
                    <Image
                      alt={organization.displayName || organization.name}
                      imageId={organization.logoImageId}
                      className={`fs-4 ${styles.image}`}
                      placeholderIcon={<MdBusiness />}
                    />
                  ) : (
                    <MdBusiness className={`fs-4 ${styles.image}`} />
                  )}
                </div>

                <div>
                  <h6 className="mb-0">{organization.displayName || organization.name}</h6>
                </div>
              </div>

              {organization.homeUrl && (
                <div className="mt-1 text-truncate d-flex flex-row align-items-baseline">
                  <small className="text-muted text-uppercase me-1">Homepage: </small>
                  <ExternalLink
                    href={organization.homeUrl}
                    className={`text-reset text-truncate lh-1 bg-transparent ${styles.externalLink}`}
                    label={`Open link ${organization.homeUrl}`}
                    btnType
                  >
                    <div className="text-truncate">{organization.homeUrl}</div>
                  </ExternalLink>
                </div>
              )}

              {organization.description && (
                <div className={`mt-2 text-muted ${styles.description}`}>{organization.description}</div>
              )}
            </div>
          )}
        </div>
      </div>

      <div className="d-flex flex-row align-items-start text-truncate">
        {props.visibleLegend && (
          <div className="d-flex flex-row align-items-baseline me-1 text-muted text-uppercase">
            <small>Org:</small>
          </div>
        )}

        <button
          className={`p-0 border-0 text-dark text-truncate flex-grow-1 bg-transparent position-relative ${styles.link} ${props.btnClassName}`}
          onClick={(e) => {
            e.preventDefault();
            history.push({
              pathname: '/packages/search',
              search: prepareQueryString({
                pageNumber: 1,
                filters: {
                  org: [props.organizationName!],
                },
                deprecated: props.deprecated,
              }),
            });
          }}
          onMouseEnter={(e) => {
            e.preventDefault();
            setOnLinkHover(true);
            openOrgInfo();
          }}
          onMouseLeave={() => {
            setFetchTimeout(null);
            cleanFetchTimeout();
            setOnLinkHover(false);
          }}
          aria-label="Organization info"
          aria-expanded={openStatus}
          aria-hidden="true"
          tabIndex={-1}
        >
          <div
            className={classnames({
              'text-truncate': isUndefined(props.multiLine) || !props.multiLine,
            })}
          >
            {props.organizationDisplayName || props.organizationName}
          </div>
        </button>
      </div>
    </div>
  );
}
Example #5
Source File: SearchRepositories.tsx    From hub with Apache License 2.0 4 votes vote down vote up
SearchRepositories = (props: Props) => {
  const inputEl = useRef<HTMLInputElement>(null);
  const dropdownRef = useRef(null);
  const itemsWrapper = useRef<HTMLDivElement | null>(null);
  const [isSearching, setIsSearching] = useState(false);
  const [repositories, setRepositories] = useState<Repository[] | null>(null);
  const [searchName, setSearchName] = useState<string>('');
  const [dropdownTimeout, setDropdownTimeout] = useState<NodeJS.Timeout | null>(null);
  const [highlightedItem, setHighlightedItem] = useState<number | null>(null);

  useOutsideClick([dropdownRef], !isNull(repositories), () => cleanSearch());

  async function searchRepositories() {
    try {
      setIsSearching(true);
      let query: SearchQuery = {
        name: searchName,
        limit: DEFAULT_LIMIT,
        offset: 0,
      };
      if (props.extraQueryParams) {
        query = { ...query, filters: props.extraQueryParams };
      }
      const data = await API.searchRepositories(query);
      setRepositories(data.items);
      setIsSearching(false);
    } catch (err: any) {
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred searching repositories, please try again later.',
        });
      } else {
        props.onAuthError();
      }
      setRepositories(null);
      setIsSearching(false);
    }
  }

  const saveSelectedRepository = (item: Repository): void => {
    setRepositories(null);
    setSearchName('');
    inputEl.current!.value = '';
    props.onSelection(item);
    setHighlightedItem(null);
  };

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearchName(e.target.value);
    setHighlightedItem(null);
  };

  const checkIfRepoIsDisabled = (item: Repository): boolean => {
    let isDisabled = false;
    if (!isUndefined(props.disabledRepositories)) {
      isDisabled =
        (!isUndefined(props.disabledRepositories.ids) && props.disabledRepositories.ids.includes(item.repositoryId!)) ||
        (!isUndefined(props.disabledRepositories.users) &&
          !isNull(item.userAlias) &&
          !isUndefined(item.userAlias) &&
          props.disabledRepositories.users.includes(item.userAlias)) ||
        (!isUndefined(props.disabledRepositories.organizations) &&
          !isNull(item.organizationName) &&
          !isUndefined(item.organizationName) &&
          props.disabledRepositories.organizations.includes(item.organizationName));
    }
    return isDisabled;
  };

  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
    switch (e.key) {
      case 'Escape':
        cleanSearch();
        return;
      case 'ArrowDown':
        updateHighlightedItem('down');
        return;
      case 'ArrowUp':
        updateHighlightedItem('up');
        return;
      case 'Enter':
        e.preventDefault();
        if (!isNull(repositories) && !isNull(highlightedItem)) {
          const selectedRepo = repositories[highlightedItem];
          if (selectedRepo && !checkIfRepoIsDisabled(selectedRepo)) {
            saveSelectedRepository(selectedRepo);
          }
        }
        return;
      default:
        return;
    }
  };

  const updateHighlightedItem = (arrow: 'up' | 'down') => {
    if (!isNull(repositories) && repositories.length > 0) {
      if (!isNull(highlightedItem)) {
        let newIndex: number = arrow === 'up' ? highlightedItem - 1 : highlightedItem + 1;
        if (newIndex > repositories.length - 1) {
          newIndex = 0;
        }
        if (newIndex < 0) {
          newIndex = repositories.length - 1;
        }
        scrollDropdown(newIndex);
        setHighlightedItem(newIndex);
      } else {
        const newIndex = arrow === 'up' ? repositories.length - 1 : 0;
        scrollDropdown(newIndex);
        setHighlightedItem(newIndex);
      }
    }
  };

  const forceFocus = (): void => {
    if (!isNull(inputEl) && !isNull(inputEl.current)) {
      inputEl.current.focus();
    }
  };

  const cleanTimeout = () => {
    if (!isNull(dropdownTimeout)) {
      clearTimeout(dropdownTimeout);
      setDropdownTimeout(null);
    }
  };

  const cleanSearch = () => {
    setRepositories(null);
    setSearchName('');
    setHighlightedItem(null);
  };

  const scrollDropdown = (index: number) => {
    if (itemsWrapper && itemsWrapper.current) {
      const itemsOnScreen = Math.floor(itemsWrapper.current.clientHeight / ITEM_HEIGHT) - 1;
      if (index + 1 > itemsOnScreen) {
        itemsWrapper.current.scroll(0, (index - itemsOnScreen) * ITEM_HEIGHT);
      } else {
        itemsWrapper.current.scroll(0, 0);
      }
    }
  };

  useEffect(() => {
    const isInputFocused = inputEl.current === document.activeElement;
    if (searchName.length >= MIN_CHARACTERS_SEARCH && isInputFocused) {
      cleanTimeout();
      setDropdownTimeout(
        setTimeout(() => {
          searchRepositories();
        }, SEARCH_DELAY)
      );
    } else {
      cleanSearch();
    }

    return () => {
      if (!isNull(dropdownTimeout)) {
        clearTimeout(dropdownTimeout);
      }
    };
  }, [searchName]); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className="position-relative">
      <div className="d-flex flex-row">
        <div
          className={`flex-grow-1 d-flex align-items-stretch overflow-hidden position-relative searchBar lh-base bg-white ${styles.inputWrapper}`}
        >
          <div
            data-testid="searchBarIcon"
            className={`d-flex align-items-center ${styles.iconWrapper}`}
            onClick={forceFocus}
          >
            <FiSearch />
          </div>

          <input
            ref={inputEl}
            type="text"
            className={`flex-grow-1 pe-4 ps-2 ps-md-0 border-0 shadow-none bg-transparent ${styles.input}`}
            name="searchRepositoriesInput"
            aria-label="Search repositories"
            autoComplete="new-input"
            onChange={onChange}
            onKeyDown={onKeyDown}
            spellCheck="false"
          />

          {isSearching && (
            <div className={`position-absolute text-secondary ${styles.loading}`}>
              <span data-testid="searchBarSpinning" className="spinner-border spinner-border-sm" />
            </div>
          )}
        </div>
      </div>

      {!isNull(repositories) && (
        <div ref={dropdownRef} className={`dropdown-menu w-100 p-0 shadow-sm show overflow-hidden ${styles.dropdown}`}>
          {repositories.length === 0 ? (
            <p className="m-3 text-center">
              We can't seem to find any repositories that match your search for{' '}
              <span className="fw-bold">{searchName}</span>
            </p>
          ) : (
            <div className={`overflow-scroll ${styles.tableWrapper}`} ref={itemsWrapper}>
              <table
                className={`table table-hover table-sm mb-0 text-break ${styles.table}`}
                role="grid"
                aria-labelledby={props.label}
              >
                <thead>
                  <tr>
                    <th scope="col" className={`${styles.fitCell} d-none d-sm-table-cell`}></th>
                    <th scope="col" className={styles.repoCell}>
                      Repository
                    </th>
                    {props.visibleUrl && (
                      <th scope="col" className="d-none d-md-table-cell">
                        Url
                      </th>
                    )}
                    <th scope="col">Publisher</th>
                  </tr>
                </thead>
                <tbody>
                  {repositories.map((item: Repository, index: number) => {
                    const isDisabled = checkIfRepoIsDisabled(item);

                    return (
                      <tr
                        data-testid="repoItem"
                        role="button"
                        className={classnames(
                          { [styles.clickableCell]: !isDisabled },
                          { [styles.disabledCell]: isDisabled },
                          { [styles.activeCell]: index === highlightedItem }
                        )}
                        onClick={() => {
                          if (!isDisabled) {
                            saveSelectedRepository(item);
                          }
                        }}
                        key={`repo_${item.name!}`}
                        onMouseOver={() => setHighlightedItem(index)}
                        onMouseOut={() => setHighlightedItem(null)}
                      >
                        <td className="align-middle text-center d-none d-sm-table-cell">
                          <div className="mx-2">
                            <RepositoryIcon kind={item.kind} className={`w-auto ${styles.icon}`} />
                          </div>
                        </td>
                        <td className="align-middle">
                          <div className={styles.truncateWrapper}>
                            <div className="text-truncate">
                              {searchName === '' ? (
                                <>{item.name}</>
                              ) : (
                                <>
                                  {regexifyString({
                                    pattern: new RegExp(escapeRegExp(searchName), 'gi'),
                                    decorator: (match: string, index: number) => {
                                      return (
                                        <span key={`match_${item.name}_${index}`} className="fw-bold highlighted">
                                          {match}
                                        </span>
                                      );
                                    },
                                    input: item.name,
                                  })}
                                </>
                              )}
                            </div>
                          </div>
                        </td>
                        {props.visibleUrl && (
                          <td className="align-middle d-none d-md-table-cell">
                            <div className={styles.truncateWrapper}>
                              <div className="text-truncate">
                                <small>{item.url}</small>
                              </div>
                            </div>
                          </td>
                        )}
                        <td className="align-middle">
                          <div className="text-dark d-flex flex-row align-items-center">
                            <span className={`me-1 ${styles.tinyIcon}`}>
                              {item.userAlias ? <FaUser /> : <MdBusiness />}
                            </span>
                            {item.userAlias || item.organizationDisplayName || item.organizationName}
                          </div>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </div>
      )}
    </div>
  );
}
Example #6
Source File: UserContext.tsx    From hub with Apache License 2.0 4 votes vote down vote up
UserContext = () => {
  const { ctx, dispatch } = useContext(AppCtx);
  const [organizations, setOrganizations] = useState<Organization[] | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const alias = ctx.user!.alias;
  const [openStatus, setOpenStatus] = useState(false);
  const ref = useRef(null);
  useOutsideClick([ref], openStatus, () => setOpenStatus(false));

  const handleChange = (value: string | Organization): void => {
    if (isString(value)) {
      authorizer.updateCtx();
      dispatch(unselectOrg());
    } else {
      authorizer.updateCtx(value.name);
      dispatch(updateOrg(value.name));
    }
    setOpenStatus(false);
  };

  async function fetchOrganizations() {
    try {
      setIsLoading(true);
      const allOrganizations = await API.getAllUserOrganizations();
      const confirmedOrganizations = allOrganizations.filter((org: Organization) => org.confirmed);
      if (ctx.prefs.controlPanel.selectedOrg) {
        const selectedOrg = confirmedOrganizations.find(
          (org: Organization) => org.name === ctx.prefs.controlPanel.selectedOrg
        );
        if (isUndefined(selectedOrg)) {
          dispatch(unselectOrg());
        } else {
          authorizer.updateCtx(ctx.prefs.controlPanel.selectedOrg);
        }
      }
      setOrganizations(confirmedOrganizations);
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setOrganizations([]);
      }
    }
  }

  useEffect(() => {
    fetchOrganizations();
    authorizer.init(ctx.prefs.controlPanel.selectedOrg);
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className={`position-relative ${styles.ctxWrapper}`}>
      <div className="d-flex flex-column">
        <small className={`text-uppercase text-muted ${styles.legendCtx}`}>Control panel context</small>
        <div className="d-flex flex-row align-items-center">
          <button
            className={`btn btn-primary rounded-pill btn-sm pe-3 position-relative lh-1 ${styles.ctxBtn}`}
            type="button"
            onClick={() => {
              fetchOrganizations();
              setOpenStatus(true);
            }}
            aria-label="Open context"
            aria-expanded={openStatus}
          >
            <div className="d-flex flex-row align-items-center">
              {!isUndefined(ctx.prefs.controlPanel.selectedOrg) ? (
                <>
                  <div className={`badge bg-light text-dark rounded-pill me-2 p-0 ${styles.badgeIcon}`}>
                    <MdBusiness />
                  </div>
                  <div className={`flex-grow-1 text-start me-1 text-truncate ${styles.badgeContent}`}>
                    {ctx.prefs.controlPanel.selectedOrg}
                  </div>
                </>
              ) : (
                <>
                  <div className={`badge bg-light text-dark rounded-pill me-2 p-0 ${styles.badgeIcon}`}>
                    <FaUser />
                  </div>
                  <div className={`flex-grow-1 text-start me-1 text-truncate ${styles.badgeContent}`}>{alias}</div>
                </>
              )}
            </div>

            <div className={`position-absolute textLight ${styles.caret}`}>
              <FaCaretDown />
            </div>
          </button>
        </div>
        {isLoading && (
          <div className={`position-absolute text-secondary ${styles.loading}`} role="status">
            <span className="spinner-border spinner-border-sm" />
          </div>
        )}
      </div>

      <div
        ref={ref}
        role="menu"
        className={classnames('dropdown-menu dropdown-menu-end', styles.dropdown, { show: openStatus })}
      >
        <div className={`dropdown-arrow ${styles.arrow}`} />

        <button className="dropdown-item mw-100" onClick={() => handleChange(alias)} aria-label="Activate user context">
          <div className="d-flex flex-row align-items-center text-truncate">
            <FaUser className={`me-2 ${styles.icon}`} />
            <div className="flex-grow-1 text-truncate">{alias}</div>
            {isUndefined(ctx.prefs.controlPanel.selectedOrg) && (
              <GoCheck className={`ms-2 text-success ${styles.icon}`} />
            )}
          </div>
        </button>
        {organizations && (
          <>
            {organizations.map((org: Organization) => (
              <button
                key={`opt_${org.name}`}
                className="dropdown-item"
                onClick={() => handleChange(org)}
                aria-label={`Activate org ${org.name} context`}
              >
                <div className="d-flex flex-row align-items-center text-truncate">
                  <MdBusiness className={`me-2 ${styles.icon}`} />
                  <div className="flex-grow-1 text-truncate">{org.name}</div>
                  {!isUndefined(ctx.prefs.controlPanel.selectedOrg) &&
                    org.name === ctx.prefs.controlPanel.selectedOrg && (
                      <GoCheck className={`ms-2 text-success ${styles.icon}`} />
                    )}
                </div>
              </button>
            ))}
          </>
        )}
      </div>
    </div>
  );
}
Example #7
Source File: Card.tsx    From hub with Apache License 2.0 4 votes vote down vote up
OrganizationCard = (props: Props) => {
  const { ctx, dispatch } = useContext(AppCtx);
  const [isLeaving, setIsLeaving] = useState(false);
  const [isAccepting, setIsAccepting] = useState(false);
  const dropdownMenu = useRef(null);
  const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
  const [leaveModalStatus, setLeaveModalStatus] = useState<boolean>(false);

  const isMember =
    !isUndefined(props.organization.confirmed) && !isNull(props.organization.confirmed) && props.organization.confirmed;

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

  useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);

  async function leaveOrganization() {
    try {
      setIsLeaving(true);
      await API.deleteOrganizationMember(props.organization.name, ctx.user!.alias);
      setIsLeaving(false);
      closeDropdown();
      props.onSuccess();
      if (
        !isUndefined(ctx.prefs.controlPanel.selectedOrg) &&
        ctx.prefs.controlPanel.selectedOrg === props.organization.name
      ) {
        dispatch(unselectOrg());
      }
    } catch (err: any) {
      setIsLeaving(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        closeDropdown();
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred leaving the organization, please try again later.',
        });
      } else {
        props.onAuthError();
      }
    }
  }

  async function confirmOrganizationMembership() {
    setIsAccepting(true);
    try {
      await API.confirmOrganizationMembership(props.organization.name);
      setIsAccepting(false);
      props.onSuccess();
    } catch {
      setIsAccepting(false);
    }
  }

  const hasDropdownContent =
    !isUndefined(props.organization) &&
    (!props.organization.confirmed ||
      (props.organization.confirmed &&
        isMember &&
        props.organization.membersCount &&
        props.organization.membersCount > 1));

  return (
    <div className="col-12 col-xxl-6 py-sm-3 py-2 px-0 px-xxl-3" data-testid="organizationCard">
      <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 align-items-start">
            <div className="d-flex flex-row align-items-center w-100">
              <div
                className={`d-flex align-items-center justify-content-center overflow-hidden p-1 me-2 position-relative border border-3 bg-white rounded-circle ${styles.imageWrapper} imageWrapper`}
              >
                {!isUndefined(props.organization.logoImageId) ? (
                  <Image
                    alt={props.organization.displayName || props.organization.name}
                    imageId={props.organization.logoImageId}
                    className={`fs-4 ${styles.image}`}
                    placeholderIcon={<MdBusiness />}
                  />
                ) : (
                  <MdBusiness className={styles.image} />
                )}
              </div>

              <div className="text-truncate">
                <div className={`h5 mb-0 text-truncate ${styles.title}`}>
                  {props.organization.displayName || props.organization.name}
                </div>
              </div>

              {!isMember && (
                <div className="ms-3">
                  <span className="badge bg-warning">Invitation not accepted yet</span>
                </div>
              )}

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

                  {props.organization.confirmed ? (
                    <>
                      {isMember && props.organization.membersCount && props.organization.membersCount > 1 && (
                        <button
                          className="dropdown-item btn btn-sm rounded-0 text-dark"
                          onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                            e.preventDefault();
                            closeDropdown();
                            setLeaveModalStatus(true);
                          }}
                          aria-label="Open modal"
                        >
                          <div className="d-flex flex-row align-items-center">
                            <FaSignOutAlt className={`me-2 ${styles.btnIcon}`} />
                            <span>Leave</span>
                          </div>
                        </button>
                      )}
                    </>
                  ) : (
                    <div>
                      <button
                        className="dropdown-item btn btn-sm rounded-0 text-dark"
                        onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                          e.preventDefault();
                          confirmOrganizationMembership();
                          closeDropdown();
                        }}
                        disabled={isAccepting}
                        aria-label="Confirm membership"
                      >
                        <div className="d-flex flex-row align-items-center">
                          {isAccepting ? (
                            <>
                              <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                              <span className="ms-2">Accepting invitation...</span>
                            </>
                          ) : (
                            <>
                              <FaEnvelopeOpenText className={`me-2 ${styles.btnIcon}`} />
                              <span>Accept invitation</span>
                            </>
                          )}
                        </div>
                      </button>
                    </div>
                  )}
                </div>

                {hasDropdownContent && (
                  <button
                    className={`ms-3 mb-2 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>

            {leaveModalStatus && (
              <Modal
                className={`d-inline-block ${styles.modal}`}
                closeButton={
                  <>
                    <button
                      className="btn btn-sm btn-outline-secondary text-uppercase"
                      onClick={() => setLeaveModalStatus(false)}
                      aria-label="Close modal"
                    >
                      <div className="d-flex flex-row align-items-center">
                        <IoMdCloseCircle className="me-2" />
                        <span>Cancel</span>
                      </div>
                    </button>

                    <button
                      className="btn btn-sm btn-danger ms-3"
                      onClick={(e) => {
                        e.preventDefault();
                        leaveOrganization();
                      }}
                      disabled={isLeaving}
                      aria-label="Leave organization"
                    >
                      <div className="d-flex flex-row align-items-center text-uppercase">
                        {isLeaving ? (
                          <>
                            <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                            <span className="ms-2">Leaving...</span>
                          </>
                        ) : (
                          <>
                            <FaSignOutAlt className={`me-2 ${styles.btnIcon}`} />
                            <span>Leave</span>
                          </>
                        )}
                      </div>
                    </button>
                  </>
                }
                header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Leave organization</div>}
                onClose={() => setLeaveModalStatus(false)}
                open
              >
                <div className="mt-3 mw-100 text-center">
                  <p>Are you sure you want to leave this organization?</p>
                </div>
              </Modal>
            )}
          </div>

          {props.organization.homeUrl && (
            <div className="mt-3 text-truncate">
              <small className="text-muted text-uppercase me-1">Homepage: </small>
              <ExternalLink
                href={props.organization.homeUrl}
                className={`text-reset ${styles.link}`}
                label={`Open link ${props.organization.homeUrl}`}
              >
                {props.organization.homeUrl}
              </ExternalLink>
            </div>
          )}

          {props.organization.description && (
            <div className="mt-2">
              <p className="mb-0">{props.organization.description}</p>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}
Example #8
Source File: Form.tsx    From hub with Apache License 2.0 4 votes vote down vote up
OrganizationForm = forwardRef<HTMLFormElement, Props>((props, ref) => {
  const { ctx, dispatch } = useContext(AppCtx);
  const [imageId, setImageId] = useState<string | undefined>(
    props.organization ? props.organization.logoImageId : undefined
  );
  const nameInput = useRef<RefInputField>(null);
  const homeUrlInput = useRef<RefInputField>(null);
  const [isValidated, setIsValidated] = useState(false);
  const [apiError, setApiError] = useState<string | null>(null);

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

  async function handleOrganization(organization: Organization) {
    try {
      if (isUndefined(props.organization)) {
        await API.addOrganization(organization);
        if (!isUndefined(props.onSuccess)) {
          props.onSuccess();
        }
      } else {
        await API.updateOrganization(organization, props.organization.name);
        if (ctx.prefs.controlPanel.selectedOrg && ctx.prefs.controlPanel.selectedOrg === props.organization.name) {
          dispatch(updateOrg(organization.name));
        }
      }
      props.setIsSending(false);
    } catch (err: any) {
      props.setIsSending(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        let error = compoundErrorMessage(
          err,
          `An error occurred ${isUndefined(props.organization) ? 'adding' : 'updating'} the organization`
        );
        if (err.kind === ErrorKind.Forbidden && !isUndefined(props.organization)) {
          error = `You do not have permissions to update the organization.`;
        }
        setApiError(error);
        if (!isUndefined(props.setApiError)) {
          props.setApiError(error);
        }
      } else {
        props.onAuthError();
      }
    }
  }

  const submitForm = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    e.stopPropagation();

    cleanApiError();
    props.setIsSending(true);
    if (e.currentTarget) {
      validateForm(e.currentTarget).then((validation: FormValidation) => {
        if (validation.isValid && !isNull(validation.organization)) {
          handleOrganization(validation.organization);
        } else {
          props.setIsSending(false);
        }
      });
    }
  };

  const validateForm = async (form: HTMLFormElement): Promise<FormValidation> => {
    let organization: Organization | null = null;

    return validateAllFields().then((isValid: boolean) => {
      if (isValid) {
        const formData = new FormData(form);
        organization = {
          name: formData.get('name') as string,
          displayName: formData.get('displayName') as string,
          homeUrl: formData.get('homeUrl') as string,
          description: formData.get('description') as string,
        };

        if (!isUndefined(imageId)) {
          organization.logoImageId = imageId;
        }
      }
      setIsValidated(true);
      return { isValid, organization };
    });
  };

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

  return (
    <form
      ref={ref}
      data-testid="organizationForm"
      className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
      onFocus={cleanApiError}
      autoComplete="on"
      onSubmit={(e: FormEvent<HTMLFormElement>) => submitForm(e)}
      noValidate
    >
      <InputFileField
        name="logo"
        label="Logo"
        labelLegend={<small className="ms-1 fst-italic">(Click on the image to update)</small>}
        value={imageId}
        placeholderIcon={<MdBusiness />}
        onImageChange={(imageId: string) => setImageId(imageId)}
        onAuthError={props.onAuthError}
      />

      <InputField
        ref={nameInput}
        type="text"
        label="Name"
        labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
        name="name"
        value={props.organization ? props.organization.name : ''}
        invalidText={{
          default: 'This field is required',
          patternMismatch: 'Only lower case letters, numbers or hyphens',
          customError: 'There is another organization with this name',
        }}
        validateOnBlur
        checkAvailability={{
          isAvailable: true,
          resourceKind: ResourceKind.organizationName,
          excluded: props.organization ? [props.organization.name] : [],
        }}
        pattern="[a-z0-9-]+"
        autoComplete="off"
        required
      />

      <InputField
        type="text"
        label="Display name"
        name="displayName"
        value={props.organization ? props.organization.displayName || '' : ''}
      />

      <InputField
        ref={homeUrlInput}
        type="url"
        label="Home URL"
        name="homeUrl"
        invalidText={{
          default: 'Please enter a valid url',
        }}
        validateOnBlur
        value={props.organization ? props.organization.homeUrl || '' : ''}
      />

      <div className="">
        <label className={`form-label fw-bold ${styles.label}`} htmlFor="description">
          Description
        </label>
        <textarea
          data-testid="descriptionTextarea"
          className="form-control"
          id="description"
          name="description"
          defaultValue={props.organization ? props.organization.description || '' : ''}
        />
      </div>

      {!isNull(apiError) && isUndefined(props.setApiError) && (
        <div className="alert alert-danger mt-3" role="alert" aria-live="assertive" aria-atomic="true">
          {apiError}
        </div>
      )}
    </form>
  );
})
Example #9
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 #10
Source File: Modal.tsx    From hub with Apache License 2.0 4 votes vote down vote up
OptOutModal = (props: Props) => {
  const { ctx } = useContext(AppCtx);
  const [apiError, setApiError] = useState<string | null>(null);
  const [repoItem, setRepoItem] = useState<Repository | null>(null);
  const [eventKind, setEventKind] = useState<EventKind>(EventKind.RepositoryTrackingErrors);
  const [isSending, setIsSending] = useState<boolean>(false);
  const [userOrganizations, setUserOrganizations] = useState<string[] | undefined>(undefined);

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

  const submitForm = () => {
    if (!isNull(repoItem)) {
      addOptOut();
    }
  };

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

  async function getAllUserOrganizations() {
    try {
      const orgs = await API.getAllUserOrganizations();
      const orgsList = orgs.map((org: Organization) => org.name);
      setUserOrganizations(orgsList);
    } catch (err: any) {
      setUserOrganizations([]);
    }
  }

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

  async function addOptOut() {
    try {
      setIsSending(true);
      await API.addOptOut(repoItem!.repositoryId!, eventKind);
      setRepoItem(null);
      setIsSending(false);
      props.onSuccess();
      props.onClose();
    } catch (err: any) {
      setIsSending(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: `An error occurred adding the opt-out entry for ${props.getNotificationTitle(
            eventKind
          )} notifications for repository ${repoItem!.displayName || repoItem!.name}, please try again later.`,
        });
      }
    }
  }

  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">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}`}>Add opt-out entry</div>}
      open={props.open}
      modalDialogClassName={styles.modal}
      closeButton={
        <button
          className="btn btn-sm btn-outline-secondary"
          type="button"
          disabled={isNull(repoItem) || isSending}
          onClick={submitForm}
          aria-label="Add opt-out entry"
        >
          {isSending ? (
            <>
              <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
              <span className="ms-2">Opting out</span>
            </>
          ) : (
            <div className="d-flex flex-row align-items-center text-uppercase">
              <IoMdLogOut className="me-2" />
              <div>Opt-out</div>
            </div>
          )}
        </button>
      }
      onClose={onCloseModal}
      error={apiError}
      cleanError={() => setApiError(null)}
      noScrollable
    >
      <div className="w-100 position-relative">
        <label className={`form-label fw-bold ${styles.label}`} htmlFor="kind">
          Events
        </label>
        <div className="d-flex flex-column flex-wrap pb-2">
          {REPOSITORY_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
            return (
              <div className="mb-2" key={`radio_${subs.name}`}>
                <div className="form-check text-nowrap my-1 my-md-0">
                  <input
                    className="form-check-input"
                    type="radio"
                    name="kind"
                    id={subs.name}
                    value={subs.kind}
                    disabled={!subs.enabled}
                    checked={subs.kind === eventKind}
                    onChange={() => setEventKind(subs.kind)}
                    required
                  />
                  <label className="form-check-label" htmlFor={subs.name}>
                    <div className="d-flex flex-row align-items-center ms-2">
                      {subs.icon}
                      <div className="ms-1">{subs.title}</div>
                    </div>
                  </label>
                </div>
              </div>
            );
          })}
        </div>

        <div className="d-flex flex-column mb-3">
          <label className={`form-label fw-bold ${styles.label}`} htmlFor="description">
            Repository
          </label>

          <small className="mb-2">Select repository:</small>

          {!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 ${styles.icon}`} />
                    </div>

                    <div className="ms-2 me-2 me-sm-0 fw-bold mb-0 text-truncate">
                      {repoItem.name}
                      <span className="d-inline d-sm-none">
                        <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 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="repo-subscriptions"
                disabledRepositories={{
                  ids: props.disabledList,
                }}
                extraQueryParams={{ user: ctx.user ? [ctx.user.alias] : [], org: userOrganizations || [] }}
                onSelection={onRepoSelect}
                onAuthError={props.onAuthError}
                visibleUrl={false}
              />
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
}
Example #11
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
RepositoriesSection = (props: Props) => {
  const title = useRef<HTMLDivElement>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [optOutList, setOptOutList] = useState<OptOutByRepo[] | undefined>(undefined);
  const [optOutFullList, setOptOutFullList] = useState<OptOutByRepo[] | undefined>(undefined);
  const [repoIdsList, setRepoIdsList] = useState<string[]>([]);
  const [optOutItems, setOptOutItems] = useState<OptOutItem[] | undefined>(undefined);
  const [modalStatus, setModalStatus] = useState<boolean>(false);
  const [activePage, setActivePage] = useState<number>(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);
    if (title && title.current) {
      title.current.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
    }
  };

  const getNotificationTitle = (kind: EventKind): string => {
    let title = '';
    const notif = REPOSITORY_SUBSCRIPTIONS_LIST.find((subs: SubscriptionItem) => subs.kind === kind);
    if (!isUndefined(notif)) {
      title = notif.title.toLowerCase();
    }
    return title;
  };

  const getVisibleOptOut = (items: OptOutByRepo[]): OptOutByRepo[] => {
    if (isUndefined(items)) return [];
    return items.slice(offset, offset + DEFAULT_LIMIT);
  };

  const sortOptOutList = (items: OptOutItem[]): OptOutByRepo[] => {
    let list: OptOutByRepo[] = [];

    items.forEach((item: OptOutItem) => {
      const itemIndex = list.findIndex(
        (obr: OptOutByRepo) => obr.repository.repositoryId === item.repository.repositoryId
      );
      if (itemIndex >= 0) {
        list[itemIndex] = { ...list[itemIndex], optOutItems: [...list[itemIndex].optOutItems, item] };
      } else {
        list.push({
          repository: item.repository,
          optOutItems: [item],
        });
      }
    });

    return sortBy(list, 'repository.name');
  };

  async function getOptOutList(callback?: () => void) {
    try {
      setIsLoading(true);
      const items = await API.getAllOptOut();
      const formattedItems = sortOptOutList(items);
      setOptOutItems(items);
      setOptOutFullList(formattedItems);
      setRepoIdsList(formattedItems ? formattedItems.map((item: OptOutByRepo) => item.repository.repositoryId!) : []);
      setTotal(formattedItems.length);
      const newVisibleItems = getVisibleOptOut(formattedItems);
      // When current page is empty after changes
      if (newVisibleItems.length === 0 && activePage !== 1) {
        onPageNumberChange(1);
      } else {
        setOptOutList(newVisibleItems);
      }
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred getting your opt-out entries list, please try again later.',
        });
        setOptOutFullList([]);
        setOptOutList([]);
      } else {
        props.onAuthError();
      }
    } finally {
      if (callback) {
        callback();
      }
    }
  }

  async function changeSubscription(changeProps: ChangeSubsProps) {
    const { data, callback } = { ...changeProps };
    try {
      if (!isUndefined(data.optOutId)) {
        await API.deleteOptOut(data.optOutId);
      } else {
        await API.addOptOut(data.repoId, data.kind);
      }
      getOptOutList(callback);
    } catch (err: any) {
      callback();
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: `An error occurred ${
            !isUndefined(data.optOutId) ? 'deleting' : 'adding'
          } the opt-out entry for ${getNotificationTitle(data.kind)} notifications for repository ${
            data.repoName
          }, please try again later.`,
        });
        getOptOutList(); // Get opt-out if changeSubscription fails
      } else {
        props.onAuthError();
      }
    }
  }

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

  useEffect(() => {
    setOptOutList(getVisibleOptOut(optOutFullList || []));
  }, [activePage]); /* eslint-disable-line react-hooks/exhaustive-deps */

  return (
    <div className="mt-5 pt-3">
      {(isUndefined(optOutList) || isLoading) && <Loading />}
      <div className="d-flex flex-row align-items-start justify-content-between pb-2">
        <div ref={title} className={`h4 pb-0 ${styles.title}`}>
          Repositories
        </div>
        <div>
          <button
            className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
            onClick={() => setModalStatus(true)}
            aria-label="Open opt-out modal"
          >
            <div className="d-flex flex-row align-items-center justify-content-center">
              <IoMdLogOut />
              <span className="d-none d-md-inline ms-2">Opt-out</span>
            </div>
          </button>
        </div>
      </div>

      <div className="mt-3 mt-md-3">
        <p>
          Repositories notifications are <span className="fw-bold">enabled by default</span>. However, you can opt-out
          of notifications for certain kinds of events that happen in any of the repositories you can manage.
        </p>

        <p>
          You will <span className="fw-bold">NOT</span> receive notifications when an event that matches any of the
          repositories in the list is fired.
        </p>

        <div className="mt-4 mt-md-5">
          {!isUndefined(optOutList) && optOutList.length > 0 && (
            <div className="row">
              <div className="col-12 col-xxxl-10">
                <table className={`table table-bordered table-hover ${styles.table}`} data-testid="repositoriesList">
                  <thead>
                    <tr className={styles.tableTitle}>
                      <th scope="col" className={`align-middle text-center d-none d-sm-table-cell ${styles.fitCell}`}>
                        Kind
                      </th>
                      <th scope="col" className="align-middle w-50">
                        Repository
                      </th>
                      <th scope="col" className="align-middle w-50 d-none d-sm-table-cell">
                        Publisher
                      </th>
                      {REPOSITORY_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => (
                        <th
                          scope="col"
                          className={`align-middle text-nowrap ${styles.fitCell}`}
                          key={`title_${subs.kind}`}
                        >
                          <div className="d-flex flex-row align-items-center justify-content-center">
                            {subs.icon}
                            {subs.shortTitle && <span className="d-inline d-lg-none ms-2">{subs.shortTitle}</span>}
                            <span className="d-none d-lg-inline ms-2">{subs.title}</span>
                          </div>
                        </th>
                      ))}
                    </tr>
                  </thead>
                  <tbody className={styles.body}>
                    {optOutList.map((item: OptOutByRepo) => {
                      const repoInfo: Repository = item.repository;
                      return (
                        <tr key={`subs_${repoInfo.repositoryId}`} data-testid="optOutRow">
                          <td className="align-middle text-center d-none d-sm-table-cell">
                            <RepositoryIcon kind={repoInfo.kind} className={`h-auto ${styles.icon}`} />
                          </td>
                          <td className="align-middle">
                            <div className="d-flex flex-row align-items-center">
                              <Link
                                data-testid="repoLink"
                                className="text-dark text-capitalize"
                                to={{
                                  pathname: '/packages/search',
                                  search: prepareQueryString({
                                    pageNumber: 1,
                                    filters: {
                                      repo: [repoInfo.name],
                                    },
                                  }),
                                }}
                              >
                                {repoInfo.name}
                              </Link>
                            </div>
                          </td>
                          <td className="align-middle position-relative d-none d-sm-table-cell">
                            <span className={`mx-1 mb-1 ${styles.tinyIcon}`}>
                              {repoInfo.userAlias ? <FaUser /> : <MdBusiness />}
                            </span>{' '}
                            {repoInfo.userAlias ? (
                              <Link
                                data-testid="userLink"
                                className="text-dark"
                                to={{
                                  pathname: '/packages/search',
                                  search: prepareQueryString({
                                    pageNumber: 1,
                                    filters: {
                                      user: [repoInfo.userAlias!],
                                    },
                                  }),
                                }}
                              >
                                {repoInfo.userAlias}
                              </Link>
                            ) : (
                              <Link
                                data-testid="orgLink"
                                className="text-dark"
                                to={{
                                  pathname: '/packages/search',
                                  search: prepareQueryString({
                                    pageNumber: 1,
                                    filters: {
                                      org: [repoInfo.organizationName!],
                                    },
                                  }),
                                }}
                              >
                                {repoInfo.organizationDisplayName || repoInfo.organizationName}
                              </Link>
                            )}
                          </td>
                          {REPOSITORY_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem, index: number) => {
                            const optItem = item.optOutItems.find((opt: OptOutItem) => subs.kind === opt.eventKind);

                            return (
                              <td className="align-middle text-center" key={`td_${repoInfo.name}_${subs.kind}`}>
                                <div className="text-center position-relative">
                                  <SubscriptionSwitch
                                    repoInfo={repoInfo}
                                    kind={subs.kind}
                                    enabled={subs.enabled}
                                    optOutItem={optItem}
                                    changeSubscription={changeSubscription}
                                  />
                                </div>
                              </td>
                            );
                          })}
                        </tr>
                      );
                    })}
                    {!isUndefined(total) && total > DEFAULT_LIMIT && (
                      <tr className={styles.paginationCell}>
                        <td className="align-middle text-center" colSpan={5}>
                          <Pagination
                            limit={DEFAULT_LIMIT}
                            offset={offset}
                            total={total}
                            active={activePage}
                            className="my-3"
                            onChange={onPageNumberChange}
                          />
                        </td>
                      </tr>
                    )}
                  </tbody>
                </table>
              </div>
            </div>
          )}
        </div>
      </div>

      {modalStatus && (
        <OptOutModal
          disabledList={repoIdsList}
          optOutList={optOutItems}
          onSuccess={getOptOutList}
          onClose={() => setModalStatus(false)}
          onAuthError={props.onAuthError}
          getNotificationTitle={getNotificationTitle}
          open
        />
      )}
    </div>
  );
}
Example #12
Source File: InProductionButton.tsx    From hub with Apache License 2.0 4 votes vote down vote up
InProductionButton = (props: Props) => {
  const { ctx, dispatch } = useContext(AppCtx);
  const history = useHistory();
  const [openStatus, setOpenStatus] = useState(false);
  const [organizations, setOrganizations] = useState<Organization[] | undefined | null>(undefined);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [updatingStatus, setUpdatingStatus] = useState<string | null>(null);

  const ref = useRef(null);
  useOutsideClick([ref], openStatus, () => setOpenStatus(false));

  async function fetchOrganizations(visibleLoading: boolean = false) {
    if (isNull(ctx.user)) {
      setOrganizations(undefined);
    } else {
      try {
        if (visibleLoading) {
          setIsLoading(true);
        }
        setOrganizations(
          await API.getProductionUsage({
            packageName: props.normalizedName,
            repositoryKind: getRepoKindName(props.repository.kind)!,
            repositoryName: props.repository.name,
          })
        );
        setOpenStatus(true);

        if (visibleLoading) {
          setIsLoading(false);
        }
      } catch (err: any) {
        setOrganizations(null);
        setOpenStatus(true);

        if (visibleLoading) {
          setIsLoading(false);
        }
        if (err.kind !== ErrorKind.Unauthorized) {
          alertDispatcher.postAlert({
            type: 'danger',
            message:
              'Something went wrong checking if your organizations use this package in production, please try again later.',
          });
        } else {
          dispatch(signOut());
          history.push(`${window.location.pathname}?modal=login&redirect=${window.location.pathname}`);
        }
      }
    }
  }

  async function changeUsage(name: string, isActive: boolean) {
    try {
      setUpdatingStatus(name);
      if (isActive) {
        await API.deleteProductionUsage(
          {
            packageName: props.normalizedName,
            repositoryKind: getRepoKindName(props.repository.kind)!,
            repositoryName: props.repository.name,
          },
          name
        );
      } else {
        await API.addProductionUsage(
          {
            packageName: props.normalizedName,
            repositoryKind: getRepoKindName(props.repository.kind)!,
            repositoryName: props.repository.name,
          },
          name
        );
      }
      alertDispatcher.postAlert({
        type: 'info',
        message: "Your change was applied successfully. It'll be visible across the site in a few minutes",
      });
      setUpdatingStatus(null);
      // We don't need to get orgs after changing it due to we are closing the dropdown
      // and we get them again every time we open the dropdown
      setOpenStatus(false);
    } catch (err: any) {
      setUpdatingStatus(null);
      setOpenStatus(false);

      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: `${
            isActive
              ? 'Something went wrong deleting the selected organization from the list of production users of this package'
              : 'Something went wrong adding the selected organization to the list of production users of this package'
          }, please try again later.`,
        });
      } else {
        dispatch(signOut());
        history.push(`${window.location.pathname}?modal=login&redirect=${window.location.pathname}`);
      }
    }
  }

  const isDisabled = isNull(ctx.user) || isUndefined(ctx.user);

  return (
    <div className="d-none d-md-block position-relative ms-2">
      <ElementWithTooltip
        active
        tooltipClassName={styles.tooltip}
        element={
          <button
            className={classnames(
              'btn btn-outline-primary rounded-circle p-0 position-relative lh-1 fs-5',
              styles.iconWrapper,
              {
                [`disabled ${styles.isDisabled}`]: isDisabled,
              }
            )}
            type="button"
            onClick={() => {
              if (!isLoading || isDisabled) {
                fetchOrganizations(true);
              }
            }}
            aria-label="Open organizations menu"
            aria-expanded={openStatus}
          >
            <div className="d-flex align-items-center justify-content-center">
              {isLoading && (
                <div className={styles.loading}>
                  <div className={`spinner-border text-primary ${styles.spinner}`} role="status">
                    <span className="visually-hidden">Loading...</span>
                  </div>
                </div>
              )}
              <RiMedalLine className={classnames('rounded-circle', { 'text-muted': isDisabled })} />
            </div>
          </button>
        }
        tooltipMessage="You must be signed in to specify which of your organizations are using this package in production"
        visibleTooltip={isDisabled}
      />

      <div
        ref={ref}
        role="menu"
        className={classnames('dropdown-menu dropdown-menu-end p-0', styles.dropdown, {
          show: openStatus,
        })}
      >
        <div
          className={classnames('dropdown-arrow', styles.arrow, {
            [styles.darkArrow]: organizations && organizations.length > 0,
          })}
        />

        {isNull(organizations) || isUndefined(organizations) || organizations.length === 0 ? (
          <div className={`p-4 text-center ${styles.emptyListMsg}`}>
            Here you'll be able to specify which of the organizations you belong to are using this package in
            production.
          </div>
        ) : (
          <div>
            <div className={`p-3 border-bottom ${styles.title}`}>
              Select which of your organizations are using this package in production
            </div>
            <div className={`overflow-auto ${styles.buttonsWrapper}`}>
              {organizations!.map((org: Organization) => {
                const isActive = org.usedInProduction || false;
                const isUpdating = !isNull(updatingStatus) && updatingStatus === org.name;

                return (
                  <button
                    className={`${styles.dropdownItem} dropdownItem btn p-3 rounded-0 w-100`}
                    onClick={() => changeUsage(org.name, isActive)}
                    key={`subs_${org.name}`}
                    aria-label={
                      isActive
                        ? `Delete ${org.displayName || org.name} organization from package's production users list`
                        : `Add ${org.displayName || org.name} organization to package's production users list`
                    }
                  >
                    <div className="d-flex flex-row align-items-start w-100 justify-content-between">
                      <div className="me-3 position-relative">
                        <span className={classnames({ 'd-none': isUpdating })}>
                          {isActive ? <FaRegCheckCircle className="text-success" /> : <FaRegCircle />}
                        </span>
                        {isUpdating && (
                          <div className="text-secondary top-0">
                            <span className="spinner-border spinner-border-sm" />
                          </div>
                        )}
                      </div>
                      <div className={`d-flex flex-column flex-grow-1 ${styles.growWidth}`}>
                        <div className="d-flex flex-row align-items-center">
                          <div className="me-2">
                            <div
                              className={`d-flex align-items-center justify-content-center overflow-hidden rounded-circle border bg-white ${styles.imageWrapper} imageWrapper`}
                            >
                              <Image
                                alt={org.displayName || org.name}
                                imageId={org.logoImageId}
                                className={`m-auto ${styles.image}`}
                                placeholderIcon={<MdBusiness className={styles.orgIcon} />}
                              />
                            </div>
                          </div>
                          <div className="h6 mb-0 mw-100 text-truncate">{org.displayName || org.name}</div>
                        </div>
                      </div>
                    </div>
                  </button>
                );
              })}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}