react-icons/md#MdAdd TypeScript Examples

The following examples show how to use react-icons/md#MdAdd. 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: Navigation.tsx    From website-docs with MIT License 6 votes vote down vote up
function TocMenu({ data, level, active }: ItemProps) {
  const [head, ...rest] = active

  const [open, setOpen] = useState(data === head)
  const content = <TocContent content={data.content} />
  const children = (
    <ul className={open ? listOpen : undefined}>
      {data.children!.map((child, index) => (
        <TocItem data={child} level={level + 1} active={rest} key={index} />
      ))}
    </ul>
  )

  if (level === 0) {
    const icon = open ? <MdSort /> : <MdMenu />
    return (
      <li className={navItem}>
        <div className={menu} onClick={() => setOpen(open => !open)}>
          {content}
          {icon}
        </div>
        {children}
      </li>
    )
  }
  const icon = open ? <MdRemove /> : <MdAdd />
  return (
    <li className={navItem}>
      <div className={menu} onClick={() => setOpen(open => !open)}>
        {icon}
        {content}
      </div>
      {children}
    </li>
  )
}
Example #2
Source File: RoomsList.tsx    From convoychat with GNU General Public License v3.0 5 votes vote down vote up
RoomsList: React.FC = () => {
  const history = useHistory();
  const { dispatch } = useModalContext();
  const [modalRoomId, setModalRoomId] = useState<string>("");

  const { data, loading, error } = useListCurrentUserRoomsQuery({
    onCompleted(data) {
      // redirect to first room on the list on initial load
      if (
        history.location.pathname === "/" &&
        data.currentUserRooms.length > 1
      ) {
        history.push(`/room/${data.currentUserRooms[0].id}`);
      }
    },
  });

  return (
    <section>
      <InviteMembers roomId={modalRoomId} />

      <Flex align="center" justify="space-between">
        <h3>Your Rooms</h3>
        <Tooltip placement="top" message={<span>Create new room</span>}>
          <IconButton
            onClick={() => dispatch({ type: "OPEN", modal: "CreateRoom" })}
            icon={<MdAdd />}
          />
        </Tooltip>
      </Flex>
      <Spacer gap="large" />

      {loading && <Loader />}
      {error && <span>Error Loading Rooms</span>}
      {data?.currentUserRooms?.map(room => {
        return (
          <RoomLink
            id={room.id}
            key={room.id}
            name={room.name}
            isSelected={!!history.location.pathname.match(room.id)}
            onInviteMemberClick={roomId => setModalRoomId(roomId)}
          />
        );
      })}
    </section>
  );
}
Example #3
Source File: Footer.tsx    From website-docs with MIT License 5 votes vote down vote up
export function Footer() {
  const { language } = useI18next()
  const [spread, setSpread] = useState<number | undefined>(undefined)

  const { FooterLogoSVG } = useStaticQuery(graphql`
    query {
      FooterLogoSVG: file(relativePath: { eq: "pingcap-logo.svg" }) {
        publicURL
      }
    }
  `)
  const footerColumns = language === 'zh' ? zh : en

  const handleSpreadItems = (index: number) => () => {
    const screenWidth = window.screen.width
    if (screenWidth > 768) {
      return
    }

    setSpread(spread === index ? undefined : index)
  }

  return (
    <BulmaFooter className={footer}>
      <Container>
        <Columns>
          {footerColumns.map((column, index) => (
            <Column key={column.name}>
              <Title
                className={title}
                size={6}
                onClick={handleSpreadItems(index)}>
                {column.name}
                <span
                  className={clsx(spreadStyle, index === spread && clicked)}>
                  <MdAdd />
                </span>
              </Title>
              <ul className={clsx(items, index === spread && displayed)}>
                {column.items.map(item => (
                  <li key={item.name}>
                    {item.url.startsWith('/') ? (
                      <Link to={item.url}>{item.name}</Link>
                    ) : (
                      <a href={item.url} target="_blank" rel="noreferrer">
                        {item.name}
                      </a>
                    )}
                  </li>
                ))}
              </ul>
            </Column>
          ))}

          <Column>
            <Columns className={socials} multiline>
              <Socials
                className={clsx('column is-4', column)}
                locale={language as Locale}
              />
            </Columns>
          </Column>
        </Columns>

        <div className={annotations}>
          <div className={copyright}>
            ©{new Date().getFullYear()} PingCAP. All Rights Reserved.
          </div>
          <a href="https://pingcap.com" target="_blank" rel="noreferrer">
            <img className={logo} src={FooterLogoSVG.publicURL} alt="PingCAP" />
          </a>
        </div>
      </Container>
    </BulmaFooter>
  )
}
Example #4
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
MembersSection = (props: Props) => {
  const history = useHistory();
  const { ctx } = useContext(AppCtx);
  const [isGettingMembers, setIsGettingMembers] = useState(false);
  const [members, setMembers] = useState<Member[] | undefined>(undefined);
  const [modalMemberOpen, setModalMemberOpen] = useState(false);
  const [confirmedMembersNumber, setConfirmedMembersNumber] = useState<number>(0);
  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}`,
    });
  };

  const getConfirmedMembersNumber = (members: Member[]): number => {
    const confirmedMembers = members.filter((member: Member) => member.confirmed);
    return confirmedMembers.length;
  };

  async function fetchMembers() {
    try {
      setIsGettingMembers(true);
      const data = await API.getOrganizationMembers(
        {
          limit: DEFAULT_LIMIT,
          offset: offset,
        },
        activeOrg!
      );
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        setMembers(data.items);
        setTotal(total);
        setConfirmedMembersNumber(getConfirmedMembersNumber(data.items));
      }
      updatePageNumber();
      setApiError(null);
      setIsGettingMembers(false);
    } catch (err: any) {
      setIsGettingMembers(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setMembers([]);
        setApiError('An error occurred getting the organization members, please try again later.');
      } else {
        props.onAuthError();
      }
    }
  }

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

  useEffect(() => {
    if (!isUndefined(members)) {
      if (activePage === 1) {
        // fetchMembers is forced when context changes
        fetchMembers();
      } else {
        // when current page is different to 1, to update page number fetchMembers 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(() => {
    fetchMembers();
  }, []); /* eslint-disable-line react-hooks/exhaustive-deps */

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

            <div>
              <ActionBtn
                className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
                contentClassName="justify-content-center"
                onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
                  e.preventDefault();
                  setModalMemberOpen(true);
                }}
                action={AuthorizerAction.AddOrganizationMember}
                label="Open invite member modal"
              >
                <div className="d-flex flex-row align-items-center">
                  <MdAdd className="d-inline d-md-none" />
                  <MdAddCircle className="d-none d-md-inline me-2" />
                  <span className="d-none d-md-inline">Invite</span>
                </div>
              </ActionBtn>
            </div>
          </div>
        </div>

        {(isGettingMembers || isUndefined(members)) && <Loading />}

        <div className="mt-5">
          {!isUndefined(members) && (
            <>
              {members.length === 0 ? (
                <NoData issuesLinkVisible={!isNull(apiError)}>
                  {isNull(apiError) ? (
                    <>
                      <p className="h6 my-4 lh-base">Do you want to add a member?</p>

                      <button
                        type="button"
                        className="btn btn-sm btn-outline-secondary"
                        onClick={() => setModalMemberOpen(true)}
                        aria-label="Open modal"
                      >
                        <div className="d-flex flex-row align-items-center text-uppercase">
                          <MdAddCircle className="me-2" />
                          <span>Add member</span>
                        </div>
                      </button>
                    </>
                  ) : (
                    <>{apiError}</>
                  )}
                </NoData>
              ) : (
                <>
                  <div className="row mt-4 mt-md-5 gx-0 gx-xxl-4">
                    {members.map((member: Member) => (
                      <MemberCard
                        key={`member_${member.alias}`}
                        member={member}
                        onAuthError={props.onAuthError}
                        onSuccess={fetchMembers}
                        membersNumber={confirmedMembersNumber}
                      />
                    ))}
                  </div>
                  {!isUndefined(total) && (
                    <div className="mx-auto">
                      <Pagination
                        limit={DEFAULT_LIMIT}
                        offset={offset}
                        total={total}
                        active={activePage}
                        className="my-5"
                        onChange={onPageNumberChange}
                      />
                    </div>
                  )}
                </>
              )}
            </>
          )}
        </div>
      </div>

      {modalMemberOpen && (
        <MemberModal
          open={modalMemberOpen}
          membersList={members}
          onSuccess={fetchMembers}
          onAuthError={props.onAuthError}
          onClose={() => setModalMemberOpen(false)}
        />
      )}
    </main>
  );
}
Example #5
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
OrganizationsSection = (props: Props) => {
  const history = useHistory();
  const [isLoading, setIsLoading] = useState(false);
  const [modalStatus, setModalStatus] = useState<ModalStatus>({
    open: false,
  });
  const [organizations, setOrganizations] = useState<Organization[] | undefined>(undefined);
  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}`,
    });
  };

  async function fetchOrganizations() {
    try {
      setIsLoading(true);
      const data = await API.getUserOrganizations({
        limit: DEFAULT_LIMIT,
        offset: offset,
      });
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        setOrganizations(data.items);
        setTotal(total);
      }
      updatePageNumber();
      setApiError(null);
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setOrganizations([]);
        setApiError('An error occurred getting your organizations, please try again later.');
      } else {
        props.onAuthError();
      }
    }
  }

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

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

  return (
    <main
      role="main"
      className="px-xs-0 px-sm-3 px-lg-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}`}>Organizations</div>

            <div>
              <button
                className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
                onClick={() => setModalStatus({ open: true })}
                aria-label="Open modal"
              >
                <div className="d-flex flex-row align-items-center justify-content-center">
                  <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>
                </div>
              </button>
            </div>
          </div>

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

          {!isUndefined(organizations) && (
            <>
              {organizations.length === 0 ? (
                <NoData issuesLinkVisible={!isNull(apiError)}>
                  {isNull(apiError) ? (
                    <>
                      <p className="h6 my-4 lh-base">Do you need to create a organization?</p>

                      <button
                        type="button"
                        className="btn btn-sm btn-outline-secondary"
                        onClick={() => setModalStatus({ open: true })}
                        aria-label="Open modal for adding first organization"
                      >
                        <div className="d-flex flex-row align-items-center text-uppercase">
                          <MdAddCircle className="me-2" />
                          <span>Add new organization</span>
                        </div>
                      </button>
                    </>
                  ) : (
                    <>{apiError}</>
                  )}
                </NoData>
              ) : (
                <>
                  <div className="row mt-4 mt-md-5 gx-0 gx-xxl-4">
                    {organizations.map((org: Organization, index: number) => (
                      <OrganizationCard
                        key={`org_${org.name}_${index}`}
                        organization={org}
                        onAuthError={props.onAuthError}
                        onSuccess={fetchOrganizations}
                      />
                    ))}
                  </div>
                  {!isUndefined(total) && (
                    <div className="mx-auto">
                      <Pagination
                        limit={DEFAULT_LIMIT}
                        offset={offset}
                        total={total}
                        active={activePage}
                        className="my-5"
                        onChange={onPageNumberChange}
                      />
                    </div>
                  )}
                </>
              )}
            </>
          )}
        </div>
      </div>

      <OrganizationModal
        {...modalStatus}
        onSuccess={fetchOrganizations}
        onAuthError={props.onAuthError}
        onClose={() => setModalStatus({ open: false })}
      />
    </main>
  );
}
Example #6
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>
  );
}
Example #7
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
APIKeysSection = (props: Props) => {
  const history = useHistory();
  const [isLoading, setIsLoading] = useState(false);
  const [apiKeysList, setApiKeysList] = useState<APIKey[] | undefined>(undefined);
  const [apiError, setApiError] = useState<string | JSX.Element | null>(null);
  const [modalStatus, setModalStatus] = useState<ModalStatus>({
    open: false,
  });
  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}`,
    });
  };

  async function getAPIKeys() {
    try {
      setIsLoading(true);
      const data = await API.getAPIKeys({
        limit: DEFAULT_LIMIT,
        offset: offset,
      });
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        setApiKeysList(data.items);
        setTotal(total);
      }
      updatePageNumber();
      setApiError(null);
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setApiError('An error occurred getting your API keys, please try again later.');
        setApiKeysList([]);
      } else {
        props.onAuthError();
      }
    }
  }

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

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

  return (
    <div className="d-flex flex-column flex-grow-1">
      {(isUndefined(apiKeysList) || isLoading) && <Loading />}

      <main role="main" className="p-0">
        <div className="flex-grow-1">
          <div className="d-flex flex-row align-items-center justify-content-between pb-2 border-bottom">
            <div className={`h3 pb-0 ${styles.title}`}>API keys</div>
            <div>
              <button
                className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
                onClick={() => setModalStatus({ open: true })}
                aria-label="Open modal to add API key"
              >
                <div className="d-flex flex-row align-items-center justify-content-center">
                  <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>
                </div>
              </button>
            </div>
          </div>

          <div className="mt-4">
            {!isUndefined(apiKeysList) && (
              <div className="mt-4 mt-md-5">
                {apiKeysList.length === 0 ? (
                  <NoData issuesLinkVisible={!isNull(apiError)}>
                    {isNull(apiError) ? (
                      <>
                        <p className="h6 my-4">Add your first API key!</p>

                        <button
                          type="button"
                          className="btn btn-sm  btn-outline-secondary"
                          onClick={() => setModalStatus({ open: true })}
                          aria-label="Open API key modal to add the first one"
                        >
                          <div className="d-flex flex-row align-items-center text-uppercase">
                            <MdAddCircle className="me-2" />
                            <span>Add API key</span>
                          </div>
                        </button>
                      </>
                    ) : (
                      <>{apiError}</>
                    )}
                  </NoData>
                ) : (
                  <>
                    <div className="row mt-4 mt-md-5 gx-0 gx-xxl-4" data-testid="apiKeysList">
                      {apiKeysList.map((apiKey: APIKey) => (
                        <APIKeyCard
                          key={apiKey.apiKeyId!}
                          apiKey={apiKey}
                          setModalStatus={setModalStatus}
                          onSuccess={getAPIKeys}
                          onAuthError={props.onAuthError}
                        />
                      ))}
                    </div>
                    {!isUndefined(total) && (
                      <div className="mx-auto">
                        <Pagination
                          limit={DEFAULT_LIMIT}
                          offset={offset}
                          total={total}
                          active={activePage}
                          className="my-5"
                          onChange={onPageNumberChange}
                        />
                      </div>
                    )}
                  </>
                )}
              </div>
            )}
          </div>

          <APIKeyModal
            {...modalStatus}
            onSuccess={getAPIKeys}
            onClose={() => setModalStatus({ open: false })}
            onAuthError={props.onAuthError}
          />
        </div>
      </main>
    </div>
  );
}
Example #8
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
PackagesSection = (props: Props) => {
  const title = useRef<HTMLDivElement>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [packages, setPackages] = useState<Package[] | 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 = PACKAGE_SUBSCRIPTIONS_LIST.find((subs: SubscriptionItem) => subs.kind === kind);
    if (notif) {
      title = notif.title.toLowerCase();
    }
    return title;
  };

  const updateSubscriptionsPackagesOptimistically = (kind: EventKind, isActive: boolean, packageId: string) => {
    const packageToUpdate = packages ? packages.find((item: Package) => item.packageId === packageId) : undefined;
    if (packageToUpdate && packageToUpdate.eventKinds) {
      const newPackages = packages!.filter((item: Package) => item.packageId !== packageId);
      if (isActive) {
        packageToUpdate.eventKinds = packageToUpdate.eventKinds.filter((notifKind: number) => notifKind !== kind);
      } else {
        packageToUpdate.eventKinds.push(kind);
      }

      if (packageToUpdate.eventKinds.length > 0) {
        newPackages.push(packageToUpdate);
      }

      setPackages(newPackages);
    }
  };

  async function getSubscriptions() {
    try {
      setIsLoading(true);
      const data = await API.getUserSubscriptions({
        limit: DEFAULT_LIMIT,
        offset: offset,
      });
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        setPackages(data.items);
        setTotal(total);
      }
      setIsLoading(false);
    } catch (err: any) {
      setIsLoading(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: 'An error occurred getting your subscriptions, please try again later.',
        });
        setPackages([]);
      } else {
        props.onAuthError();
      }
    }
  }

  async function changeSubscription(packageId: string, kind: EventKind, isActive: boolean, packageName: string) {
    updateSubscriptionsPackagesOptimistically(kind, isActive, packageId);

    try {
      if (isActive) {
        await API.deleteSubscription(packageId, kind);
      } else {
        await API.addSubscription(packageId, kind);
      }
      getSubscriptions();
    } catch (err: any) {
      if (err.kind !== ErrorKind.Unauthorized) {
        alertDispatcher.postAlert({
          type: 'danger',
          message: `An error occurred ${isActive ? 'unsubscribing from' : 'subscribing to'} ${getNotificationTitle(
            kind
          )} notification for ${packageName} package, please try again later.`,
        });
        getSubscriptions(); // Get subscriptions if changeSubscription fails
      } else {
        props.onAuthError();
      }
    }
  }

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

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

      <div className="d-flex flex-row align-items-start justify-content-between pb-2 mt-5">
        <div ref={title} className={`h4 mb-0 ${styles.title}`}>
          Packages
        </div>
        <div>
          <button
            className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
            onClick={() => setModalStatus(true)}
            aria-label="Open subscription modal"
          >
            <div className="d-flex flex-row align-items-center justify-content-center">
              <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>
            </div>
          </button>
        </div>
      </div>

      <div className="mx-auto mt-3 mt-md-3">
        <p className="m-0">
          You will receive an email notification when an event that matches any of the subscriptions in the list is
          fired.
        </p>

        <div className="mt-4 mt-md-5">
          {!isUndefined(packages) && packages.length > 0 && (
            <>
              <div className="d-none d-sm-inline" data-testid="packagesList">
                <div className="row">
                  <div className="col-12 col-xxxl-10">
                    <table className={`table table-bordered table-hover ${styles.table}`}>
                      <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">
                            Package
                          </th>
                          <th scope="col" className="align-middle w-50">
                            Publisher
                          </th>
                          {PACKAGE_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}
                                <span className="d-none d-lg-inline ms-2">{subs.title}</span>
                              </div>
                            </th>
                          ))}
                        </tr>
                      </thead>
                      <tbody className={styles.body}>
                        {packages.map((item: Package) => (
                          <tr key={`subs_${item.packageId}`} data-testid="subsTableCell">
                            <td className="align-middle text-center d-none d-sm-table-cell">
                              <RepositoryIcon kind={item.repository.kind} className={`h-auto ${styles.icon}`} />
                            </td>
                            <td className="align-middle">
                              <div className="d-flex flex-row align-items-center">
                                <div
                                  className={`d-flex align-items-center justify-content-center overflow-hidden p-1 rounded-circle border bg-white ${styles.imageWrapper} imageWrapper`}
                                >
                                  <Image
                                    alt={item.displayName || item.name}
                                    imageId={item.logoImageId}
                                    className={`fs-4 ${styles.image}`}
                                    kind={item.repository.kind}
                                  />
                                </div>

                                <Link
                                  data-testid="packageLink"
                                  className="ms-2 text-dark"
                                  to={{
                                    pathname: buildPackageURL(item.normalizedName, item.repository, item.version!),
                                  }}
                                  aria-label={`Open ${item.displayName || item.name} package`}
                                >
                                  {item.displayName || item.name}
                                </Link>
                              </div>
                            </td>
                            <td className="align-middle position-relative">
                              {item.repository.userAlias ? (
                                <Link
                                  data-testid="userLink"
                                  className="text-dark"
                                  to={{
                                    pathname: '/packages/search',
                                    search: prepareQueryString({
                                      pageNumber: 1,
                                      filters: {
                                        user: [item.repository.userAlias!],
                                      },
                                    }),
                                  }}
                                  aria-label={`Filter by ${item.repository.userAlias} user`}
                                >
                                  {item.repository.userAlias}
                                </Link>
                              ) : (
                                <Link
                                  data-testid="orgLink"
                                  className="text-dark"
                                  to={{
                                    pathname: '/packages/search',
                                    search: prepareQueryString({
                                      pageNumber: 1,
                                      filters: {
                                        org: [item.repository.organizationName!],
                                      },
                                    }),
                                  }}
                                  aria-label={`Filter by ${
                                    item.repository.organizationDisplayName || item.repository.organizationName
                                  } organization`}
                                >
                                  {item.repository.organizationDisplayName || item.repository.organizationName}
                                </Link>
                              )}

                              <small className="ms-2">
                                (<span className={`text-uppercase text-muted ${styles.legend}`}>Repo: </span>
                                <Link
                                  data-testid="repoLink"
                                  className="text-dark"
                                  to={{
                                    pathname: '/packages/search',
                                    search: prepareQueryString({
                                      pageNumber: 1,
                                      filters: {
                                        repo: [item.repository.name],
                                      },
                                    }),
                                  }}
                                  aria-label={`Filter by ${
                                    item.repository.displayName || item.repository.name
                                  } repository`}
                                >
                                  {item.repository.displayName || item.repository.name}
                                </Link>
                                )
                              </small>
                            </td>
                            {PACKAGE_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
                              const isActive = !isUndefined(item.eventKinds) && item.eventKinds.includes(subs.kind);
                              const id = `subs_${item.packageId}_${subs.kind}`;

                              return (
                                <td className="align-middle text-center" key={`td_${item.normalizedName}_${subs.kind}`}>
                                  <div className="text-center">
                                    <div className="form-switch">
                                      <input
                                        data-testid={`${item.name}_${subs.name}_input`}
                                        id={id}
                                        type="checkbox"
                                        role="switch"
                                        className={`form-check-input ${styles.checkbox}`}
                                        disabled={!subs.enabled}
                                        onChange={() =>
                                          changeSubscription(
                                            item.packageId,
                                            subs.kind,
                                            isActive,
                                            item.displayName || item.name
                                          )
                                        }
                                        checked={isActive}
                                      />
                                    </div>
                                  </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 className="d-inline d-sm-none">
                {packages.map((item: Package) => (
                  <PackageCard key={item.packageId} package={item} changeSubscription={changeSubscription} />
                ))}
                {!isUndefined(total) && (
                  <div className="mx-auto">
                    <Pagination
                      limit={DEFAULT_LIMIT}
                      offset={offset}
                      total={total}
                      active={activePage}
                      className="my-5"
                      onChange={onPageNumberChange}
                    />
                  </div>
                )}
              </div>
            </>
          )}
        </div>
      </div>

      <SubscriptionModal
        open={modalStatus}
        subscriptions={packages}
        onSuccess={getSubscriptions}
        onClose={() => setModalStatus(false)}
        getNotificationTitle={getNotificationTitle}
      />
    </>
  );
}
Example #9
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
WebhooksSection = (props: Props) => {
  const history = useHistory();
  const { ctx } = useContext(AppCtx);
  const [isGettingWebhooks, setIsGettingWebhooks] = useState(false);
  const [webhooks, setWebhooks] = useState<Webhook[] | undefined>(undefined);
  const [apiError, setApiError] = useState<null | string>(null);
  const [visibleForm, setVisibleForm] = useState<VisibleForm | null>(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}`,
    });
  };
  async function fetchWebhooks() {
    try {
      setIsGettingWebhooks(true);
      const data = await API.getWebhooks(
        {
          limit: DEFAULT_LIMIT,
          offset: offset,
        },
        ctx.prefs.controlPanel.selectedOrg
      );
      const total = parseInt(data.paginationTotalCount);
      if (total > 0 && data.items.length === 0) {
        onPageNumberChange(1);
      } else {
        setWebhooks(data.items);
        setTotal(total);
      }
      updatePageNumber();
      setApiError(null);
      setIsGettingWebhooks(false);
    } catch (err: any) {
      setIsGettingWebhooks(false);
      if (err.kind !== ErrorKind.Unauthorized) {
        setWebhooks([]);
        setApiError('An error occurred getting webhooks, please try again later.');
      } else {
        props.onAuthError();
      }
    }
  }

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

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

  return (
    <div className="d-flex flex-column flex-grow-1">
      <main role="main" className="p-0">
        <div className="flex-grow-1">
          {!isNull(visibleForm) ? (
            <WebhookForm
              onClose={() => setVisibleForm(null)}
              onSuccess={fetchWebhooks}
              webhook={visibleForm.webhook}
              {...props}
            />
          ) : (
            <>
              <div>
                <div className="d-flex flex-row align-items-center justify-content-between pb-2 border-bottom">
                  <div className={`h3 pb-0 ${styles.title}`}>Webhooks</div>

                  <div>
                    <button
                      className={`btn btn-outline-secondary btn-sm text-uppercase ${styles.btnAction}`}
                      onClick={() => setVisibleForm({ visible: true })}
                      aria-label="Open webhook form"
                    >
                      <div className="d-flex flex-row align-items-center justify-content-center">
                        <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>
                      </div>
                    </button>
                  </div>
                </div>
              </div>

              {(isGettingWebhooks || isUndefined(webhooks)) && <Loading />}

              <div className="mt-4 mt-md-5">
                <p className="m-0">Webhooks notify external services when certain events happen.</p>

                {!isUndefined(webhooks) && (
                  <>
                    {webhooks.length === 0 ? (
                      <NoData issuesLinkVisible={!isNull(apiError)}>
                        {isNull(apiError) ? (
                          <>
                            <p className="h6 my-4 lh-base">
                              You have not created any webhook yet. You can create your first one by clicking on the
                              button below.
                            </p>

                            <button
                              type="button"
                              className="btn btn-sm btn-outline-secondary"
                              onClick={() => setVisibleForm({ visible: true })}
                              aria-label="Open form for creating your first webhook"
                            >
                              <div className="d-flex flex-row align-items-center text-uppercase">
                                <MdAddCircle className="me-2" />
                                <span>Add webhook</span>
                              </div>
                            </button>
                          </>
                        ) : (
                          <>{apiError}</>
                        )}
                      </NoData>
                    ) : (
                      <>
                        <div className="row mt-3 mt-md-4 gx-0 gx-xxl-4">
                          {webhooks.map((webhook: Webhook) => (
                            <WebhookCard
                              key={`webhook_${webhook.webhookId}`}
                              webhook={webhook}
                              onEdition={() => setVisibleForm({ visible: true, webhook: webhook })}
                              onAuthError={props.onAuthError}
                              onDeletion={fetchWebhooks}
                            />
                          ))}
                        </div>
                        {!isUndefined(total) && (
                          <div className="mx-auto">
                            <Pagination
                              limit={DEFAULT_LIMIT}
                              offset={offset}
                              total={total}
                              active={activePage}
                              className="my-5"
                              onChange={onPageNumberChange}
                            />
                          </div>
                        )}
                      </>
                    )}
                  </>
                )}
              </div>
            </>
          )}
        </div>
      </main>
    </div>
  );
}