react-icons/ri#RiArrowLeftRightLine TypeScript Examples
The following examples show how to use
react-icons/ri#RiArrowLeftRightLine.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Card.tsx From hub with Apache License 2.0 | 4 votes |
RepositoryCard = (props: Props) => {
const history = useHistory();
const { ctx } = useContext(AppCtx);
const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
const [transferModalStatus, setTransferModalStatus] = useState<boolean>(false);
const [deletionModalStatus, setDeletionModalStatus] = useState<boolean>(false);
const [badgeModalStatus, setBadgeModalStatus] = useState<boolean>(false);
const dropdownMenu = useRef(null);
const organizationName = ctx.prefs.controlPanel.selectedOrg;
const hasErrors = !isUndefined(props.repository.lastTrackingErrors) && !isNull(props.repository.lastTrackingErrors);
const hasScanningErrors =
!isUndefined(props.repository.lastScanningErrors) && !isNull(props.repository.lastScanningErrors);
const [openErrorsModal, setOpenErrorsModal] = useState<boolean>(false);
const [openScanningErrorsModal, setOpenScanningErrorsModal] = useState<boolean>(false);
const closeDropdown = () => {
setDropdownMenuStatus(false);
};
useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);
useEffect(() => {
if (props.visibleModal) {
if (props.visibleModal === 'scanning') {
setOpenScanningErrorsModal(true);
} else {
setOpenErrorsModal(true);
}
history.replace({
search: '',
});
}
}, []); /* eslint-disable-line react-hooks/exhaustive-deps */
const getLastTracking = (): JSX.Element => {
const nextCheckTime: number = minutesToNearestInterval(30);
if (isUndefined(props.repository.lastTrackingTs) || isNull(props.repository.lastTrackingTs)) {
return (
<>
Not processed yet
{props.repository.disabled
? '.'
: nextCheckTime > 0
? `, it will be processed automatically in ~ ${nextCheckTime} minutes`
: ', it will be processed automatically in less than 30 minutes'}
</>
);
}
const content = (
<>
{!isFuture(props.repository.lastTrackingTs!) && (
<span>{moment.unix(props.repository.lastTrackingTs!).fromNow()}</span>
)}
{hasErrors ? (
<>
<FaExclamation className="mx-1 text-warning" />
<RepositoryWarningModal />
</>
) : (
<FaCheck className="mx-1 text-success" />
)}
</>
);
let nextCheckMsg: string = '';
if (nextCheckTime > 0 && !props.repository.disabled) {
nextCheckMsg = `(it will be checked for updates again in ~ ${nextCheckTime} minutes)`;
}
if (hasErrors) {
return (
<>
{content}
<Modal
modalDialogClassName={styles.modalDialog}
modalClassName="mh-100"
className={`d-inline-block ${styles.modal}`}
buttonType={`ms-1 btn badge btn-outline-secondary ${styles.btn}`}
buttonContent={
<div className="d-flex flex-row align-items-center">
<HiExclamation className="me-2" />
<span className="d-none d-xl-inline d-xxl-none d-xxxl-inline">Show tracking errors log</span>
<span className="d-inline d-xl-none d-xxl-inline d-xxxl-none">Logs</span>
</div>
}
header={
<div className={`h3 m-2 flex-grow-1 text-truncate ${styles.title}`}>
Tracking errors log - {props.repository.displayName || props.repository.name}
</div>
}
open={openErrorsModal}
onClose={() => setOpenErrorsModal(false)}
footerClassName={styles.modalFooter}
>
<div className="d-flex h-100 mw-100 overflow-hidden">
<div className="d-flex flex-column w-100">
<div className={`mb-2 ${styles.trackingTime}`}>
{moment.unix(props.repository.lastTrackingTs!).format('llll Z')}
</div>
<div
className={`position-relative flex-grow-1 mw-100 mh-100 overflow-hidden ${styles.modalSyntaxTrackerWrapper}`}
>
{props.repository.lastTrackingErrors && (
<SyntaxHighlighter
language="bash"
style={tomorrowNight}
customStyle={{ fontSize: '90%', height: '100%' }}
>
{props.repository.lastTrackingErrors}
</SyntaxHighlighter>
)}
</div>
</div>
</div>
</Modal>
<span className="ms-3 fst-italic text-muted">{nextCheckMsg}</span>
</>
);
} else {
return (
<>
{content}
{openErrorsModal && (
<Modal
className={`d-inline-block ${styles.modal}`}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Tracking errors log</div>}
open
>
<div className="h5 text-center my-5 mw-100">
It looks like the last tracking of this repository worked fine and no errors were produced.
<br />
<br />
If you have arrived to this screen from an email listing some errors, please keep in mind those may have
been already solved.
</div>
</Modal>
)}
<span className="ms-1 fst-italic text-muted">{nextCheckMsg}</span>
</>
);
}
};
const getLastScanning = (): JSX.Element => {
const nextCheckTime: number = minutesToNearestInterval(30, 15);
if (
props.repository.scannerDisabled ||
isUndefined(props.repository.lastTrackingTs) ||
isNull(props.repository.lastTrackingTs)
)
return <>-</>;
if (isUndefined(props.repository.lastScanningTs) || isNull(props.repository.lastScanningTs)) {
return (
<>
Not scanned yet
{props.repository.disabled
? '.'
: nextCheckTime > 0
? `, it will be scanned for security vulnerabilities in ~ ${nextCheckTime} minutes`
: ', it will be scanned for security vulnerabilities in less than 30 minutes'}
</>
);
}
const content = (
<>
{!isFuture(props.repository.lastScanningTs!) && (
<span>{moment.unix(props.repository.lastScanningTs!).fromNow()}</span>
)}
{hasScanningErrors ? (
<FaExclamation className="mx-2 text-warning" />
) : (
<FaCheck className="mx-2 text-success" />
)}
</>
);
let nextCheckMsg: string = '';
if (nextCheckTime > 0 && !props.repository.disabled) {
nextCheckMsg = `(it will be checked for updates again in ~ ${nextCheckTime} minutes)`;
}
if (hasScanningErrors) {
return (
<>
{content}
<Modal
modalDialogClassName={styles.modalDialog}
modalClassName="mh-100"
className={`d-inline-block ${styles.modal}`}
buttonType={`ms-1 btn badge btn-outline-secondary ${styles.btn}`}
buttonContent={
<div className="d-flex flex-row align-items-center">
<HiExclamation className="me-2" />
<span className="d-none d-sm-inline">Show scanning errors log</span>
<span className="d-inline d-sm-none">Logs</span>
</div>
}
header={
<div className={`h3 m-2 flex-grow-1 text-truncate ${styles.title}`}>
Scanning errors log - {props.repository.displayName || props.repository.name}
</div>
}
open={openScanningErrorsModal}
onClose={() => setOpenErrorsModal(false)}
footerClassName={styles.modalFooter}
>
<div className="d-flex h-100 mw-100 overflow-hidden">
<div className={`d-flex overflow-scroll ${styles.modalSyntaxWrapper}`}>
{props.repository.lastScanningErrors && (
<SyntaxHighlighter
language="bash"
style={tomorrowNight}
customStyle={{ fontSize: '90%', height: '100%', marginBottom: '0' }}
>
{props.repository.lastScanningErrors}
</SyntaxHighlighter>
)}
</div>
</div>
</Modal>
<span className="ms-3 fst-italic text-muted">{nextCheckMsg}</span>
</>
);
} else {
return (
<>
{content}
{openScanningErrorsModal && (
<Modal
className={`d-inline-block ${styles.modal}`}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Scanning errors log</div>}
open
>
<div className="h5 text-center my-5 mw-100">
It looks like the last security vulnerabilities scan of this repository worked fine and no errors were
produced.
<br />
<br />
If you have arrived to this screen from an email listing some errors, please keep in mind those may have
been already solved.
</div>
</Modal>
)}
<span className="ms-1 fst-italic text-muted">{nextCheckMsg}</span>
</>
);
}
};
return (
<div className="col-12 col-xxl-6 py-sm-3 py-2 px-0 px-xxl-3" data-testid="repoCard">
<div className="card h-100">
<div className="card-body d-flex flex-column h-100">
<div className="d-flex flex-row w-100 justify-content-between">
<div className={`text-truncate h5 mb-0 ${styles.titleCard}`}>
{props.repository.displayName || props.repository.name}
</div>
<OfficialBadge
official={props.repository.official}
className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
type="repo"
/>
<VerifiedPublisherBadge
verifiedPublisher={props.repository.verifiedPublisher}
className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
/>
<DisabledRepositoryBadge
disabled={props.repository.disabled!}
className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
/>
<ScannerDisabledRepositoryBadge
scannerDisabled={props.repository.scannerDisabled!}
className={`ms-3 d-none d-md-inline ${styles.labelWrapper}`}
/>
{transferModalStatus && (
<TransferRepositoryModal
open={true}
repository={props.repository}
onSuccess={props.onSuccess}
onAuthError={props.onAuthError}
onClose={() => setTransferModalStatus(false)}
/>
)}
{deletionModalStatus && (
<DeletionModal
repository={props.repository}
organizationName={organizationName}
setDeletionModalStatus={setDeletionModalStatus}
onSuccess={props.onSuccess}
onAuthError={props.onAuthError}
/>
)}
{badgeModalStatus && (
<BadgeModal
repository={props.repository}
onClose={() => setBadgeModalStatus(false)}
open={badgeModalStatus}
/>
)}
<div className="ms-auto ps-3">
<RepositoryIconLabel kind={props.repository.kind} isPlural />
</div>
<div className="ms-3">
<div
ref={dropdownMenu}
className={classnames('dropdown-menu dropdown-menu-end p-0', styles.dropdownMenu, {
show: dropdownMenuStatus,
})}
>
<div className={`dropdown-arrow ${styles.arrow}`} />
<button
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
setBadgeModalStatus(true);
}}
aria-label="Open badge modal"
>
<div className="d-flex flex-row align-items-center">
<MdLabel className={`me-2 ${styles.btnIcon}`} />
<span>Get badge</span>
</div>
</button>
<ActionBtn
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
setTransferModalStatus(true);
}}
action={AuthorizerAction.TransferOrganizationRepository}
label="Open transfer repository modal"
>
<>
<RiArrowLeftRightLine className={`me-2 ${styles.btnIcon}`} />
<span>Transfer</span>
</>
</ActionBtn>
<ActionBtn
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
props.setModalStatus({
open: true,
repository: props.repository,
});
}}
action={AuthorizerAction.UpdateOrganizationRepository}
label="Open update repository modal"
>
<>
<FaPencilAlt className={`me-2 ${styles.btnIcon}`} />
<span>Edit</span>
</>
</ActionBtn>
<ActionBtn
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
setDeletionModalStatus(true);
}}
action={AuthorizerAction.DeleteOrganizationRepository}
label="Open delete repository modal"
>
<>
<FaTrashAlt className={`me-2 ${styles.btnIcon}`} />
<span>Delete</span>
</>
</ActionBtn>
</div>
<button
className={`btn btn-outline-secondary rounded-circle p-0 text-center ${styles.btnDropdown}`}
onClick={() => setDropdownMenuStatus(true)}
aria-label="Open menu"
aria-expanded={dropdownMenuStatus}
>
<BsThreeDotsVertical />
</button>
</div>
</div>
{props.repository.repositoryId && (
<div className="mt-2 d-flex flex-row align-items-baseline">
<div className="text-truncate">
<small className="text-muted text-uppercase me-1">ID: </small>
<small>{props.repository.repositoryId}</small>
</div>
<div className={`ms-1 ${styles.copyBtn}`}>
<div className={`position-absolute ${styles.copyBtnWrapper}`}>
<ButtonCopyToClipboard
text={props.repository.repositoryId}
className="btn-link border-0 text-dark fw-bold"
label="Copy repository ID to clipboard"
/>
</div>
</div>
</div>
)}
<div className="text-truncate">
<small className="text-muted text-uppercase me-1">Url: </small>
<small>{props.repository.url}</small>
</div>
<div>
<small className="text-muted text-uppercase me-1">Last processed: </small>
<small>{getLastTracking()}</small>
</div>
<div>
<small className="text-muted text-uppercase me-1">Last security scan: </small>
<small>{getLastScanning()}</small>
</div>
<div className="mt-3 m-md-0 d-flex flex-row d-md-none">
<OfficialBadge official={props.repository.official} className="me-3" type="repo" />
<VerifiedPublisherBadge verifiedPublisher={props.repository.verifiedPublisher} className="me-3" />
<DisabledRepositoryBadge disabled={props.repository.disabled!} />
</div>
</div>
</div>
</div>
);
}
Example #2
Source File: ClaimOwnershipModal.tsx From hub with Apache License 2.0 | 4 votes |
ClaimRepositoryOwnerShipModal = (props: Props) => {
const { ctx } = useContext(AppCtx);
const siteName = getMetaTag('siteName');
const form = useRef<HTMLFormElement>(null);
const [isFetchingOrgs, setIsFetchingOrgs] = useState(false);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const [apiOrgsError, setApiOrgsError] = useState<string | null>(null);
const [apiReposError, setApiReposError] = useState<string | null>(null);
const organizationName = ctx.prefs.controlPanel.selectedOrg;
const [selectedClaimOption, setSelectedClaimOption] = useState<'org' | 'user'>(
!isUndefined(organizationName) ? 'org' : 'user'
);
const [claimingOrg, setClaimingOrg] = useState<string>(organizationName || '');
const [organizations, setOrganizations] = useState<Organization[] | undefined>(undefined);
const [repoItem, setRepoItem] = useState<Repository | null>(null);
const handleOrgChange = (event: ChangeEvent<HTMLSelectElement>) => {
setClaimingOrg(event.target.value);
setSelectedClaimOption(event.target.value === '' ? 'user' : 'org');
};
const handleClaimingFromOpt = (type: 'user' | 'org') => {
if (type === 'user') {
setClaimingOrg('');
}
setSelectedClaimOption(type);
};
// Clean API error when form is focused after validation
const cleanApiError = () => {
if (!isNull(apiError)) {
setApiError(null);
setApiReposError(null);
setApiOrgsError(null);
}
};
const onCloseModal = () => {
props.onClose();
};
const onRepoSelect = (repo: Repository): void => {
setRepoItem(repo);
};
async function claimRepository() {
try {
await API.claimRepositoryOwnership(repoItem!, claimingOrg || undefined);
if (!isUndefined(props.onSuccess)) {
props.onSuccess();
}
setIsSending(false);
onCloseModal();
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(err, 'An error occurred claiming the repository');
if (err.kind === ErrorKind.Forbidden) {
error =
'You do not have permissions to claim this repository ownership. Please make sure your metadata file has been setup correctly.';
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
const submitForm = () => {
cleanApiError();
setIsSending(true);
if (form.current && validateForm(form.current)) {
claimRepository();
} else {
setIsSending(false);
}
};
const validateForm = (form: HTMLFormElement): boolean => {
setIsValidated(true);
return form.checkValidity();
};
const getOrgsNames = (): string[] => {
if (organizations) {
return organizations.map((org: Organization) => org.name);
}
return [];
};
useEffect(() => {
async function fetchOrganizations() {
try {
setIsFetchingOrgs(true);
const orgs = await API.getAllUserOrganizations();
const confirmedOrganizations = orgs.filter((org: Organization) => org.confirmed);
setOrganizations(confirmedOrganizations);
setApiOrgsError(null);
setIsFetchingOrgs(false);
} catch (err: any) {
setIsFetchingOrgs(false);
if (err.kind !== ErrorKind.Unauthorized) {
setOrganizations([]);
setApiOrgsError('An error occurred getting your organizations, please try again later.');
} else {
props.onAuthError();
}
}
}
fetchOrganizations();
}, [organizationName, props]);
const getPublisher = (repo: Repository) => (
<small className="ms-0 ms-sm-2">
<span className="d-none d-sm-inline">(</span>
<small className={`d-none d-md-inline text-muted me-1 text-uppercase ${styles.legend}`}>Publisher: </small>
<div className={`d-inline me-1 ${styles.tinyIcon}`}>{repo.userAlias ? <FaUser /> : <MdBusiness />}</div>
<span>{repo.userAlias || repo.organizationDisplayName || repo.organizationName}</span>
<span className="d-none d-sm-inline">)</span>
</small>
);
return (
<Modal
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Claim repository ownership</div>}
open={props.open}
modalClassName={styles.modal}
size="xl"
closeButton={
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending || isNull(repoItem)}
onClick={submitForm}
aria-label="Claim ownership"
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Claiming ownership...</span>
</>
) : (
<div className="text-uppercase d-flex flex-row align-items-center">
<RiArrowLeftRightLine className="me-2" />
<div>Claim ownership</div>
</div>
)}
</button>
}
onClose={onCloseModal}
error={apiOrgsError || apiReposError || apiError}
cleanError={cleanApiError}
noScrollable
>
<div className="w-100">
<div className="mt-4">
<p>
Before claiming a repository ownership, we need to verify that you actually own it. To prove that, you need
to add a{' '}
<ExternalLink
href="https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml"
className="text-primary fw-bold"
label="Open documentation"
>
metadata file
</ExternalLink>{' '}
to your repository and include yourself (or the person who will do the request) as an owner. This will be
checked during the ownership claim process. Please make sure the email used in the metadata file matches
with the one you use in {siteName}.
</p>
</div>
<form
data-testid="claimRepoForm"
ref={form}
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
onFocus={cleanApiError}
autoComplete="on"
noValidate
>
<div>
<div className="d-flex flex-column my-3">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="description">
Repository:
</label>
{!isNull(repoItem) ? (
<div
data-testid="activeRepoItem"
className={`border border-secondary w-100 rounded mt-1 ${styles.repoWrapper}`}
>
<div className="d-flex flex-row flex-nowrap align-items-stretch justify-content-between">
<div className="flex-grow-1 text-truncate py-2">
<div className="d-flex flex-row align-items-center h-100 text-truncate">
<div className="d-none d-md-inline">
<RepositoryIcon kind={repoItem.kind} className={`mx-3 w-auto ${styles.icon}`} />
</div>
<div className="ms-2 fw-bold mb-0 text-truncate text-muted">
<span className="text-dark">{repoItem.name}</span>{' '}
<small className="text-muted">({repoItem.url})</small>
<span className={`d-inline d-sm-none ${styles.legend}`}>
<span className="mx-2">/</span>
{getPublisher(repoItem)}
</span>
</div>
<div className="px-2 ms-auto w-50 text-dark text-truncate d-none d-sm-inline">
{getPublisher(repoItem)}
</div>
</div>
</div>
<div>
<button
className={`btn btn-close btn-sm h-100 rounded-0 border-start px-3 py-0 ${styles.closeButton}`}
onClick={() => setRepoItem(null)}
aria-label="Close"
></button>
</div>
</div>
</div>
) : (
<div className={`mt-2 ${styles.searchWrapper}`}>
<SearchRepositories
label="claim-repo-ownership"
disabledRepositories={{
users: ctx.user ? [ctx.user.alias] : [],
organizations: getOrgsNames(),
}}
onSelection={onRepoSelect}
onAuthError={props.onAuthError}
visibleUrl
/>
</div>
)}
</div>
<label id="claiming" className={`form-label fw-bold ${styles.label}`}>
Transfer to:
</label>
<div className="form-check mb-2">
<input
aria-labelledby="claiming user"
className="form-check-input"
type="radio"
name="claim"
id="user"
value="user"
checked={selectedClaimOption === 'user'}
onChange={() => handleClaimingFromOpt('user')}
required
/>
<label id="user" className={`form-check-label ${styles.label}`} htmlFor="user">
My user
</label>
</div>
<div className="form-check mb-3">
<input
aria-labelledby="claiming org"
className="form-check-input"
type="radio"
name="claim"
id="org"
value="org"
checked={selectedClaimOption === 'org'}
onChange={() => handleClaimingFromOpt('org')}
required
/>
<label id="org" className={`form-check-label ${styles.label}`} htmlFor="org">
Organization
</label>
</div>
</div>
<div className="d-flex flex-row align-items-center position-relative mb-3">
<div className=" w-75 mb-2">
<select
className="form-select"
aria-label="org-select"
value={claimingOrg}
onChange={handleOrgChange}
required={selectedClaimOption === 'org'}
>
{!isUndefined(organizations) && (
<>
<option value="">Select organization</option>
{organizations.map((org: Organization) => (
<option key={`opt_${org.name}`} value={org.name}>
{org.name}
</option>
))}
</>
)}
</select>
<div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
</div>
{isFetchingOrgs && (
<div className="d-inline ms-3">
<span className="spinner-border spinner-border-sm text-primary" />
</div>
)}
</div>
<small className="text-muted text-break mt-3">
<p>It may take a few minutes for this change to be visible across the Hub.</p>
</small>
</form>
</div>
</Modal>
);
}
Example #3
Source File: TransferModal.tsx From hub with Apache License 2.0 | 4 votes |
TransferRepositoryModal = (props: Props) => {
const { ctx } = useContext(AppCtx);
const form = useRef<HTMLFormElement>(null);
const [isFetchingOrgs, setIsFetchingOrgs] = useState(false);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const organizationName = ctx.prefs.controlPanel.selectedOrg;
const [selectedTransferOption, setSelectedTransferOption] = useState<'org' | 'user'>(
isUndefined(organizationName) ? 'org' : 'user'
);
const [orgToTransfer, setOrgToTransfer] = useState<string | undefined>(undefined);
const [organizations, setOrganizations] = useState<Organization[] | undefined>(undefined);
const handleOrgChange = (event: ChangeEvent<HTMLSelectElement>) => {
setOrgToTransfer(event.target.value || undefined);
};
// Clean API error when form is focused after validation
const cleanApiError = () => {
if (!isNull(apiError)) {
setApiError(null);
}
};
const onCloseModal = () => {
props.onClose();
};
async function transferRepository() {
try {
await API.transferRepository({
repositoryName: props.repository.name,
toOrgName: orgToTransfer,
fromOrgName: organizationName,
});
if (!isUndefined(props.onSuccess)) {
props.onSuccess();
}
setIsSending(false);
onCloseModal();
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(err, 'An error occurred transferring the repository');
if (!isUndefined(organizationName) && err.kind === ErrorKind.Forbidden) {
error = 'You do not have permissions to transfer a repository to the organization.';
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
const submitForm = () => {
cleanApiError();
setIsSending(true);
if (form.current && validateForm(form.current)) {
transferRepository();
} else {
setIsSending(false);
}
};
const validateForm = (form: HTMLFormElement): boolean => {
setIsValidated(true);
return form.checkValidity();
};
useEffect(() => {
async function fetchOrganizations() {
try {
setIsFetchingOrgs(true);
let orgs = await API.getAllUserOrganizations();
if (!isUndefined(organizationName)) {
orgs = orgs.filter((org: Organization) => org.name !== organizationName);
}
setOrganizations(orgs);
setApiError(null);
setIsFetchingOrgs(false);
} catch (err: any) {
setIsFetchingOrgs(false);
if (err.kind !== ErrorKind.Unauthorized) {
setOrganizations([]);
setApiError('An error occurred getting your organizations, please try again later.');
} else {
props.onAuthError();
}
}
}
fetchOrganizations();
}, [organizationName, props]);
return (
<Modal
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Transfer repository</div>}
open={props.open}
modalClassName={styles.modal}
closeButton={
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label="Transfer repository"
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Transferring repository...</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<RiArrowLeftRightLine className="me-2" />
<span>Transfer</span>
</div>
)}
</button>
}
onClose={onCloseModal}
error={apiError}
cleanError={cleanApiError}
>
<div className="w-100">
<form
data-testid="transferRepoForm"
ref={form}
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
onFocus={cleanApiError}
autoComplete="on"
noValidate
>
{!isUndefined(organizationName) ? (
<>
<div className="form-check mb-3">
<input
className="form-check-input"
type="radio"
name="transfer"
id="user"
value="user"
checked={selectedTransferOption === 'user'}
onChange={() => setSelectedTransferOption('user')}
required
/>
<label className={`form-check-label fw-bold ${styles.label}`} htmlFor="user">
Transfer to my user
</label>
</div>
<div className="form-check mb-3">
<input
className="form-check-input"
type="radio"
name="transfer"
id="org"
value="org"
checked={selectedTransferOption === 'org'}
onChange={() => setSelectedTransferOption('org')}
required
/>
<label className={`form-check-label fw-bold ${styles.label}`} htmlFor="org">
Transfer to organization
</label>
</div>
</>
) : (
<label className={`form-label fw-bold ${styles.label}`}>Transfer to organization</label>
)}
<div
data-testid="selectOrgsWrapper"
className={classnames('d-flex flex-row align-items-center position-relative mb-3', {
invisible: selectedTransferOption === 'user',
})}
>
<div className=" w-75 mb-2">
<select
className="form-select"
aria-label="org-select"
value={orgToTransfer}
onChange={handleOrgChange}
required={selectedTransferOption === 'org'}
>
{!isUndefined(organizations) && (
<>
<option value="">Select organization</option>
{organizations.map((org: Organization) => (
<option key={`opt_${org.name}`} value={org.name}>
{org.name}
</option>
))}
</>
)}
</select>
<div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
</div>
{isFetchingOrgs && (
<div className="d-inline ms-3" role="status">
<span className="spinner-border spinner-border-sm text-primary" />
</div>
)}
</div>
<small className="text-muted text-break mt-3">
<p>It may take a few minutes for this change to be visible across the Hub.</p>
</small>
</form>
</div>
</Modal>
);
}
Example #4
Source File: index.tsx From hub with Apache License 2.0 | 4 votes |
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>
);
}