react-icons/fa#FaPencilAlt TypeScript Examples
The following examples show how to use
react-icons/fa#FaPencilAlt.
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: Modal.tsx From hub with Apache License 2.0 | 5 votes |
OrganizationModal = (props: Props) => {
const form = useRef<HTMLFormElement>(null);
const [isSending, setIsSending] = useState(false);
const [apiError, setApiError] = useState<null | string>(null);
const onCloseModal = () => {
props.onClose();
};
const submitForm = () => {
if (form.current) {
form.current.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
};
return (
<Modal
header={
<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>
{isUndefined(props.organization) ? <>Add organization</> : <>Update organization</>}
</div>
}
open={props.open}
modalClassName={styles.modal}
closeButton={
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label={`${isUndefined(props.organization) ? 'Add' : 'Update'} organization`}
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">
{isUndefined(props.organization) ? <>Adding organization</> : <>Updating organization</>}
</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
{isUndefined(props.organization) ? (
<>
<MdAddCircle className="me-2" />
<div>Add</div>
</>
) : (
<>
<FaPencilAlt className="me-2" />
<div>Update</div>
</>
)}
</div>
)}
</button>
}
onClose={onCloseModal}
error={apiError}
cleanError={() => setApiError(null)}
>
<div className="w-100">
<OrganizationForm
ref={form}
organization={props.organization}
onSuccess={() => {
if (!isUndefined(props.onSuccess)) {
props.onSuccess();
}
onCloseModal();
}}
setIsSending={setIsSending}
onAuthError={props.onAuthError}
setApiError={setApiError}
/>
</div>
</Modal>
);
}
Example #2
Source File: UpdateOrg.tsx From hub with Apache License 2.0 | 5 votes |
UpdateOrganization = (props: Props) => {
const form = useRef<HTMLFormElement>(null);
const [isSending, setIsSending] = useState(false);
const submitForm = () => {
if (form.current) {
form.current.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
};
return (
<>
{!props.isLoading && (
<OrganizationForm
ref={form}
organization={!props.isLoading ? props.organization : undefined}
onAuthError={props.onAuthError}
onSuccess={props.onSuccess}
setIsSending={setIsSending}
/>
)}
<div className="mt-4">
<ActionBtn
className="btn btn-sm btn-outline-secondary"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
submitForm();
}}
action={AuthorizerAction.UpdateOrganization}
disabled={isSending}
label="Update organization modal"
>
<>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Updating organization</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<FaPencilAlt className="me-2" />
<div>Update</div>
</div>
)}
</>
</ActionBtn>
</div>
</>
);
}
Example #3
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 #4
Source File: Modal.tsx From hub with Apache License 2.0 | 4 votes |
RepositoryModal = (props: Props) => {
const { ctx } = useContext(AppCtx);
const form = useRef<HTMLFormElement>(null);
const nameInput = useRef<RefInputField>(null);
const urlInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const organizationName = ctx.prefs.controlPanel.selectedOrg;
const [isDisabled, setIsDisabled] = useState<boolean>(props.repository ? props.repository.disabled! : false);
const [isScannerDisabled, setIsScannerDisabled] = useState<boolean>(
props.repository ? props.repository.scannerDisabled! : false
);
const [visibleDisabledConfirmation, setVisibleDisabledConfirmation] = useState<boolean>(false);
const [selectedKind, setSelectedKind] = useState<RepositoryKind>(
isUndefined(props.repository) ? DEFAULT_SELECTED_REPOSITORY_KIND : props.repository.kind
);
const [isValidInput, setIsValidInput] = useState<boolean>(false);
const [urlContainsTreeTxt, setUrlContainsTreeTxt] = useState<boolean>(false);
const [resetFields, setResetFields] = useState<boolean>(false);
const [authUser, setAuthUser] = useState<string | null>(props.repository ? props.repository.authUser || null : null);
const [authPass, setAuthPass] = useState<string | null>(props.repository ? props.repository.authPass || null : null);
const prepareTags = (): ContainerTag[] => {
if (props.repository) {
if (props.repository.data && props.repository.data.tags && !isEmpty(props.repository.data.tags)) {
return props.repository.data.tags.map((tag: ContainerTag) => {
return { ...tag, id: nanoid() };
});
} else {
return [];
}
} else {
// By default, we add mutable tag latest for new container images
return [{ name: 'latest', mutable: true, id: nanoid() }];
}
};
const [containerTags, setContainerTags] = useState<ContainerTag[]>(prepareTags());
const [repeatedTagNames, setRepeatedTagNames] = useState<boolean>(false);
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setIsValidInput(e.target.value === props.repository!.name);
};
const onUrlInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setUrlContainsTreeTxt(e.target.value.includes('/tree/'));
};
const allowPrivateRepositories: boolean = getMetaTag('allowPrivateRepositories', true);
const siteName = getMetaTag('siteName');
// Clean API error when form is focused after validation
const cleanApiError = () => {
if (!isNull(apiError)) {
setApiError(null);
}
};
const onCloseModal = () => {
props.onClose();
};
async function handleRepository(repository: Repository) {
try {
if (isUndefined(props.repository)) {
await API.addRepository(repository, organizationName);
} else {
await API.updateRepository(repository, 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 ${isUndefined(props.repository) ? 'adding' : 'updating'} the repository`
);
if (!isUndefined(organizationName) && err.kind === ErrorKind.Forbidden) {
error = `You do not have permissions to ${isUndefined(props.repository) ? 'add' : 'update'} the repository ${
isUndefined(props.repository) ? 'to' : 'from'
} the organization.`;
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
const submitForm = () => {
cleanApiError();
setIsSending(true);
if (form.current) {
validateForm(form.current).then((validation: FormValidation) => {
if (validation.isValid && !isNull(validation.repository)) {
handleRepository(validation.repository);
} else {
setIsSending(false);
}
});
}
};
const validateForm = async (form: HTMLFormElement): Promise<FormValidation> => {
let repository: Repository | null = null;
return validateAllFields().then((isValid: boolean) => {
if (isValid) {
const formData = new FormData(form);
repository = {
kind: selectedKind,
name: !isUndefined(props.repository) ? props.repository.name : (formData.get('name') as string),
url: formData.get('url') as string,
branch: formData.get('branch') as string,
displayName: formData.get('displayName') as string,
disabled: isDisabled,
scannerDisabled: isScannerDisabled,
authUser:
!isUndefined(props.repository) &&
props.repository.private &&
!resetFields &&
selectedKind === RepositoryKind.Helm
? '='
: authUser,
authPass: !isUndefined(props.repository) && props.repository.private && !resetFields ? '=' : authPass,
};
if (selectedKind === RepositoryKind.Container) {
const cleanTags = containerTags.filter((tag: ContainerTag) => tag.name !== '');
const readyTags = cleanTags.map((tag: ContainerTag) => ({ name: tag.name, mutable: tag.mutable }));
repository.data = {
tags: readyTags,
};
}
}
setIsValidated(true);
return { isValid, repository };
});
};
const checkContainerTags = (): boolean => {
if (selectedKind !== RepositoryKind.Container) return true;
const tagNames = compact(containerTags.map((tag: ContainerTag) => tag.name));
const uniqTagNames = uniq(tagNames);
if (tagNames.length === uniqTagNames.length) {
setRepeatedTagNames(false);
return true;
} else {
setRepeatedTagNames(true);
return false;
}
};
const validateAllFields = async (): Promise<boolean> => {
return Promise.all([
nameInput.current!.checkIsValid(),
urlInput.current!.checkIsValid(),
checkContainerTags(),
]).then((res: boolean[]) => {
return every(res, (isValid: boolean) => isValid);
});
};
const handleOnReturnKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter' && !isNull(form)) {
submitForm();
}
};
const handleKindChange = (event: ChangeEvent<HTMLSelectElement>) => {
setSelectedKind(parseInt(event.target.value));
// URL is validated when value has been entered previously
if (urlInput.current!.getValue() !== '') {
urlInput.current!.checkIsValid();
}
};
const getAdditionalInfo = (): JSX.Element | undefined => {
let link: JSX.Element | undefined;
switch (selectedKind) {
case RepositoryKind.Helm:
link = (
<ExternalLink
href="/docs/topics/repositories#helm-charts-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Helm charts repositories
</ExternalLink>
);
break;
case RepositoryKind.OLM:
link = (
<ExternalLink
href="/docs/topics/repositories#olm-operators-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
OLM operators repositories
</ExternalLink>
);
break;
case RepositoryKind.Falco:
link = (
<ExternalLink
href="/docs/topics/repositories#falco-rules-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Falco rules repositories
</ExternalLink>
);
break;
case RepositoryKind.OPA:
link = (
<ExternalLink
href="/docs/topics/repositories#opa-policies-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
OPA policies repositories
</ExternalLink>
);
break;
case RepositoryKind.TBAction:
link = (
<ExternalLink
href="/docs/topics/repositories#tinkerbell-actions-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Tinkerbell actions
</ExternalLink>
);
break;
case RepositoryKind.Krew:
link = (
<ExternalLink
href="/docs/topics/repositories#krew-kubectl-plugins-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Krew kubectl plugins
</ExternalLink>
);
break;
case RepositoryKind.HelmPlugin:
link = (
<ExternalLink
href="/docs/topics/repositories#helm-plugins-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Helm plugins
</ExternalLink>
);
break;
case RepositoryKind.TektonTask:
link = (
<ExternalLink
href="/docs/topics/repositories#tekton-tasks-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
<u>Tekton tasks</u>
</ExternalLink>
);
break;
case RepositoryKind.KedaScaler:
link = (
<ExternalLink
href="/docs/topics/repositories#keda-scalers-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
KEDA scalers
</ExternalLink>
);
break;
case RepositoryKind.CoreDNS:
link = (
<ExternalLink
href="/docs/topics/repositories#coredns-plugins-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
CoreDNS plugins
</ExternalLink>
);
break;
case RepositoryKind.Keptn:
link = (
<ExternalLink
href="/docs/topics/repositories#keptn-integrations-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Keptn integrations
</ExternalLink>
);
break;
case RepositoryKind.TektonPipeline:
link = (
<ExternalLink
href="/docs/topics/repositories#tekton-pipelines-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Tekton pipelines
</ExternalLink>
);
break;
case RepositoryKind.Container:
link = (
<ExternalLink
href="/docs/topics/repositories#container-images-repositories"
className="text-primary fw-bold"
label="Open documentation"
>
Container images
</ExternalLink>
);
break;
}
if (isUndefined(link)) return;
return (
<div className="text-muted text-break mt-1 mb-4">
<small>
{(() => {
switch (selectedKind) {
case RepositoryKind.Falco:
case RepositoryKind.OLM:
case RepositoryKind.OPA:
case RepositoryKind.TBAction:
case RepositoryKind.Krew:
case RepositoryKind.HelmPlugin:
case RepositoryKind.TektonTask:
case RepositoryKind.KedaScaler:
case RepositoryKind.CoreDNS:
case RepositoryKind.Keptn:
case RepositoryKind.TektonPipeline:
return (
<>
<p
className={classnames('mb-2 opacity-100', {
[styles.animatedWarning]: urlContainsTreeTxt,
})}
>
Please DO NOT include the git hosting platform specific parts, like
<span className="fw-bold">tree/branch</span>, just the path to your packages like it would show in
the filesystem.
</p>
<p className="mb-0">
For more information about the url format and the repository structure, please see the {link}{' '}
section in the{' '}
<ExternalLink
href="/docs/repositories"
className="text-primary fw-bold"
label="Open documentation"
>
repositories guide
</ExternalLink>
.
</p>
</>
);
case RepositoryKind.Container:
return (
<>
<p className="mb-3">
The url <span className="fw-bold">must</span> follow the following format:
</p>
<p className="mb-3 ms-3">
<code className={`me-2 ${styles.code}`}>oci://registry/[namespace]/repository</code> (example:{' '}
<span className="fst-italic">oci://index.docker.io/artifacthub/ah</span>)
</p>
<p>
The registry host is required, please use <code className={styles.code}>index.docker.io</code>{' '}
when referring to repositories hosted in the Docker Hub. The url should not contain any tag. For
more information please see the {link} section in the{' '}
<ExternalLink
href="/docs/repositories"
className="text-primary fw-bold"
label="Open documentation"
>
repositories guide
</ExternalLink>
.
</p>
</>
);
default:
return (
<p className="mb-0">
For more information about the url format and the repository structure, please see the {link}{' '}
section in the{' '}
<ExternalLink href="/docs/repositories" className="text-primary fw-bold" label="Open documentation">
repositories guide
</ExternalLink>
.
</p>
);
}
})()}
</small>
</div>
);
};
const getURLPattern = (): string | undefined => {
switch (selectedKind) {
case RepositoryKind.Helm:
return undefined;
case RepositoryKind.OLM:
return `(^(https://([A-Za-z0-9_.-]+)/|${OCI_PREFIX})[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)/?(.*)$`;
case RepositoryKind.Container:
return `^(${OCI_PREFIX})[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)/?(.*)$`;
default:
return '^(https://([A-Za-z0-9_.-]+)/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)/?(.*)$';
}
};
const renderPrivateFields = (): JSX.Element => (
<>
{(() => {
switch (selectedKind) {
case RepositoryKind.Helm:
case RepositoryKind.Container:
return (
<div className="row">
<InputField
className="col-sm-12 col-md-6"
type="text"
label="Username"
name="authUser"
autoComplete="off"
value={authUser || ''}
onChange={onAuthUserChange}
/>
<InputField
className="col-sm-12 col-md-6"
type="password"
label="Password"
name="authPass"
autoComplete="new-password"
value={authPass || ''}
onChange={onAuthPassChange}
visiblePassword
/>
</div>
);
default:
return (
<div>
<InputField
type="text"
label="Authentication token"
name="authPass"
additionalInfo={
<small className="text-muted text-break mt-1">
<p className="mb-0">Authentication token used in private git based repositories.</p>
</small>
}
value={authPass || ''}
onChange={onAuthPassChange}
/>
</div>
);
}
})()}
</>
);
const resetAuthFields = () => {
setResetFields(true);
setAuthPass(null);
setAuthUser(null);
};
const onAuthUserChange = (e: ChangeEvent<HTMLInputElement>) => {
setAuthUser(e.target.value);
};
const onAuthPassChange = (e: ChangeEvent<HTMLInputElement>) => {
setAuthPass(e.target.value);
};
return (
<Modal
header={
<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>
{isUndefined(props.repository) ? (
<>Add repository</>
) : (
<>{visibleDisabledConfirmation ? 'Disable repository' : 'Update repository'}</>
)}
</div>
}
open={props.open}
modalClassName={classnames(styles.modal, { [styles.allowPrivateModal]: allowPrivateRepositories })}
closeButton={
<>
{visibleDisabledConfirmation ? (
<>
<button
type="button"
className={`btn btn-sm btn-success ${styles.btnLight}`}
onClick={() => {
setVisibleDisabledConfirmation(false);
setIsValidInput(false);
}}
aria-label="Cancel"
>
<span>I'll leave it enabled</span>
</button>
<button
type="button"
className={classnames(
'btn btn-sm ms-3',
{ 'btn-outline-secondary': !isValidInput },
{ 'btn-danger': isValidInput }
)}
onClick={(e) => {
e.preventDefault();
setIsDisabled(!isDisabled);
setVisibleDisabledConfirmation(false);
}}
disabled={!isValidInput}
aria-label="Disable repository"
>
<span>I understand, continue</span>
</button>
</>
) : (
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending || visibleDisabledConfirmation}
onClick={submitForm}
aria-label={`${isUndefined(props.repository) ? 'Add' : 'Update'} repository`}
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Validating repository...</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
{isUndefined(props.repository) ? (
<>
<MdAddCircle className="me-2" />
<div>Add</div>
</>
) : (
<>
<FaPencilAlt className="me-2" />
<div>Update</div>
</>
)}
</div>
)}
</button>
)}
</>
}
onClose={onCloseModal}
error={apiError}
cleanError={cleanApiError}
>
<div className="w-100">
{visibleDisabledConfirmation ? (
<>
<div className="alert alert-warning my-4">
<span className="fw-bold text-uppercase">Important:</span> Please read this carefully.
</div>
<p>If you disable this repository all packages belonging to it will be deleted.</p>
<p>
All information related to the packages in your repository will be permanently deleted as well. This
includes packages' stars, subscriptions, webhooks, events and notifications.{' '}
<span className="fw-bold">This operation cannot be undone.</span>
</p>
<p>
You can enable back your repository at any time and the information available in the source repository
will be indexed and made available in {siteName} again.
</p>
<p>
Please type <span className="fw-bold">{props.repository!.name}</span> to confirm:
</p>
<InputField type="text" name="repoName" autoComplete="off" value="" onChange={onInputChange} />
</>
) : (
<form
data-testid="repoForm"
ref={form}
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
onFocus={cleanApiError}
autoComplete="on"
noValidate
>
<div className="w-75 mb-4">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="repoKind">
Kind
</label>
<select
className="form-select"
aria-label="kind-select"
name="repoKind"
value={selectedKind.toString()}
onChange={handleKindChange}
disabled={!isUndefined(props.repository)}
required
>
{REPOSITORY_KINDS.map((repoKind: RepoKindDef) => {
return (
<option key={`kind_${repoKind.label}`} value={repoKind.kind}>
{repoKind.name}
</option>
);
})}
</select>
</div>
<InputField
ref={nameInput}
type="text"
label="Name"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="name"
value={!isUndefined(props.repository) ? props.repository.name : ''}
readOnly={!isUndefined(props.repository)}
invalidText={{
default: 'This field is required',
patternMismatch: 'Only lower case letters, numbers or hyphens. Must start with a letter',
customError: 'There is another repository with this name',
}}
validateOnBlur
checkAvailability={{
isAvailable: true,
resourceKind: ResourceKind.repositoryName,
excluded: !isUndefined(props.repository) ? [props.repository.name] : [],
}}
pattern="[a-z][a-z0-9-]*"
autoComplete="off"
disabled={!isUndefined(props.repository)}
additionalInfo={
<small className="text-muted text-break mt-1">
<p className="mb-0">
This name will appear in your packages' urls and <span className="fw-bold">cannot be updated</span>{' '}
once is saved.
</p>
</small>
}
required
/>
<InputField
type="text"
label="Display name"
name="displayName"
value={
!isUndefined(props.repository) && !isNull(props.repository.displayName)
? props.repository.displayName
: ''
}
/>
<InputField
ref={urlInput}
type="url"
label="Url"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="url"
value={props.repository ? props.repository.url || '' : ''}
invalidText={{
default: 'This field is required',
typeMismatch: 'Please enter a valid url',
patternMismatch: 'Please enter a valid repository url for this repository kind',
customError: 'There is another repository using this url',
}}
onKeyDown={handleOnReturnKeyDown}
validateOnBlur
checkAvailability={{
isAvailable: true,
resourceKind: ResourceKind.repositoryURL,
excluded: props.repository ? [props.repository.url] : [],
}}
placeholder={selectedKind === RepositoryKind.Container ? OCI_PREFIX : ''}
pattern={getURLPattern()}
onChange={onUrlInputChange}
required
/>
{getAdditionalInfo()}
{selectedKind === RepositoryKind.Container && (
<TagsList
tags={containerTags}
setContainerTags={setContainerTags}
repeatedTagNames={repeatedTagNames}
setRepeatedTagNames={setRepeatedTagNames}
/>
)}
{[
RepositoryKind.Falco,
RepositoryKind.OLM,
RepositoryKind.OPA,
RepositoryKind.TBAction,
RepositoryKind.Krew,
RepositoryKind.HelmPlugin,
RepositoryKind.TektonTask,
RepositoryKind.KedaScaler,
RepositoryKind.CoreDNS,
RepositoryKind.Keptn,
RepositoryKind.TektonPipeline,
].includes(selectedKind) && (
<div>
<InputField
type="text"
label="Branch"
name="branch"
placeholder="master"
additionalInfo={
<small className="text-muted text-break mt-1">
<p className="mb-0">
Branch used in git based repositories. The <span className="fw-bold">master</span> branch is
used by default when none is provided.
</p>
</small>
}
value={!isUndefined(props.repository) && props.repository.branch ? props.repository.branch : ''}
/>
</div>
)}
{allowPrivateRepositories && (
<>
{props.repository && props.repository.private ? (
<>
{!resetFields ? (
<div className="mt-1 mb-4">
<div className={`fw-bold mb-2 ${styles.label}`}>Credentials</div>
<small>
<p className="mb-0 text-muted text-break">
This repository is private and has some credentials set. Current credentials cannot be
viewed, but you can{' '}
<button
type="button"
className={`btn btn-link btn-sm p-0 m-0 text-primary fw-bold position-relative d-inline-block ${styles.btnInline}`}
onClick={resetAuthFields}
aria-label="Reset credentials"
>
reset them
</button>
.
</p>
</small>
</div>
) : (
<>{renderPrivateFields()}</>
)}
</>
) : (
<>{renderPrivateFields()}</>
)}
</>
)}
<div className="mb-4">
<div className="form-check form-switch ps-0">
<label htmlFor="disabledRepo" className={`form-check-label fw-bold ${styles.label}`}>
Disabled
</label>
<input
id="disabledRepo"
type="checkbox"
className="form-check-input position-absolute ms-2"
role="switch"
value="true"
onChange={() => {
// Confirmation content is displayed when an existing repo is going to be disabled and it was not disabled before
if (props.repository && !isDisabled && !props.repository.disabled) {
setVisibleDisabledConfirmation(true);
} else {
setIsDisabled(!isDisabled);
}
}}
checked={isDisabled}
/>
</div>
<div className="form-text text-muted mt-2">
Use this switch to disable the repository temporarily or permanently.
</div>
</div>
{[
RepositoryKind.Helm,
RepositoryKind.Falco,
RepositoryKind.OLM,
RepositoryKind.OPA,
RepositoryKind.TBAction,
RepositoryKind.TektonTask,
RepositoryKind.KedaScaler,
RepositoryKind.CoreDNS,
RepositoryKind.Keptn,
RepositoryKind.TektonPipeline,
RepositoryKind.Container,
].includes(selectedKind) && (
<div className="mt-4 mb-3">
<div className="form-check form-switch ps-0">
<label htmlFor="scannerDisabledRepo" className={`form-check-label fw-bold ${styles.label}`}>
Security scanner disabled
</label>{' '}
<input
id="scannerDisabledRepo"
type="checkbox"
className="form-check-input position-absolute ms-2"
value="true"
role="switch"
onChange={() => setIsScannerDisabled(!isScannerDisabled)}
checked={isScannerDisabled}
/>
</div>
<div className="form-text text-muted mt-2">
Use this switch to disable the security scanning of the packages in this repository.
</div>
</div>
)}
</form>
)}
</div>
</Modal>
);
}
Example #5
Source File: index.tsx From hub with Apache License 2.0 | 4 votes |
AuthorizationSection = (props: Props) => {
const { ctx, dispatch } = useContext(AppCtx);
const siteName = getMetaTag('siteName');
const updateActionBtn = useRef<RefActionBtn>(null);
const [apiError, setApiError] = useState<string | JSX.Element | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [isTesting, setIsTesting] = useState<boolean>(false);
const [savedOrgPolicy, setSavedOrgPolicy] = useState<OrganizationPolicy | undefined>(undefined);
const [orgPolicy, setOrgPolicy] = useState<OrganizationPolicy | undefined | null>(undefined);
const [invalidPolicy, setInvalidPolicy] = useState<boolean>(false);
const [invalidPolicyDataJSON, setInvalidPolicyDataJSON] = useState<boolean>(false);
const [selectedOrg, setSelectedOrg] = useState<string | undefined>(undefined);
const [members, setMembers] = useState<string[] | undefined>(undefined);
const [notGetPolicyAllowed, setNotGetPolicyAllowed] = useState<boolean>(false);
const [updatePolicyAllowed, setUpdatePolicyAllowed] = useState<boolean>(false);
const [confirmationModal, setConfirmationModal] = useState<ConfirmationModal>({ open: false });
const getPredefinedPolicy = (name?: string): AuthorizationPolicy | undefined => {
let policy = PREDEFINED_POLICIES.find((item: AuthorizationPolicy) => item.name === name);
if (!isUndefined(policy) && !isUndefined(members)) {
policy = {
...policy,
data: {
roles: {
...policy.data.roles,
owner: {
users: members,
},
},
},
};
}
return policy;
};
const onPayloadChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
let updatedOrgPolicy: OrganizationPolicy | undefined = undefined;
if (value === 'predefined') {
if (savedOrgPolicy && savedOrgPolicy.predefinedPolicy) {
updatedOrgPolicy = {
...savedOrgPolicy,
authorizationEnabled: true,
};
} else {
const defaultPolicy = getPredefinedPolicy(DEFAULT_POLICY_NAME);
if (defaultPolicy) {
updatedOrgPolicy = {
...orgPolicy!,
customPolicy: null,
predefinedPolicy: defaultPolicy.name,
policyData: stringifyPolicyData(defaultPolicy.data),
};
}
}
checkPolicyChanges(
() => setOrgPolicy(updatedOrgPolicy!),
PolicyChangeAction.OnSwitchFromCustomToPredefinedPolicy
);
} else {
let updatedOrgPolicy: OrganizationPolicy | undefined = undefined;
if (savedOrgPolicy && savedOrgPolicy.customPolicy) {
updatedOrgPolicy = {
...savedOrgPolicy,
authorizationEnabled: true,
};
} else {
updatedOrgPolicy = {
...orgPolicy!,
customPolicy: null,
predefinedPolicy: null,
policyData: null,
};
}
checkPolicyChanges(
() => setOrgPolicy(updatedOrgPolicy!),
PolicyChangeAction.OnSwitchFromPredefinedToCustomPolicy
);
}
};
const checkIfUnsavedChanges = (): boolean => {
const lostData = checkUnsavedPolicyChanges(savedOrgPolicy!, orgPolicy!);
return lostData.lostData;
};
async function triggerTestInRegoPlayground() {
try {
setIsTesting(true);
let policy: string = '';
if (orgPolicy!.predefinedPolicy) {
const predefined = getPredefinedPolicy(orgPolicy!.predefinedPolicy);
if (predefined) {
policy = predefined.policy;
}
} else {
policy = orgPolicy!.customPolicy || '';
}
const data = prepareRegoPolicyForPlayground(policy, JSON.parse(orgPolicy!.policyData!), ctx.user!.alias);
const share: RegoPlaygroundResult = await API.triggerTestInRegoPlayground(data);
const popup = window.open(share.result, '_blank');
if (isNull(popup)) {
alertDispatcher.postAlert({
type: 'warning',
message:
'You have Pop-up windows blocked for this site. Please allow them so that we can open the OPA Playground for you.',
});
}
setIsTesting(false);
} catch (err: any) {
setIsTesting(false);
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred opening the Playground, please try again later.',
});
}
}
async function getAuthorizationPolicy() {
try {
setIsLoading(true);
const policy = await API.getAuthorizationPolicy(selectedOrg!);
const formattedPolicy = {
authorizationEnabled: policy.authorizationEnabled,
predefinedPolicy: policy.predefinedPolicy || null,
customPolicy: policy.customPolicy || null,
policyData: policy.policyData ? stringifyPolicyData(policy.policyData) : null,
};
setSavedOrgPolicy(formattedPolicy);
setOrgPolicy(formattedPolicy);
setNotGetPolicyAllowed(false);
setUpdatePolicyAllowed(
authorizer.check({
organizationName: selectedOrg!,
action: AuthorizerAction.UpdateAuthorizationPolicy,
user: ctx.user!.alias,
})
);
setIsLoading(false);
} catch (err: any) {
setIsLoading(false);
if (err.kind === ErrorKind.Unauthorized) {
props.onAuthError();
} else if (err.kind === ErrorKind.Forbidden) {
setNotGetPolicyAllowed(true);
setOrgPolicy(null);
} else {
setNotGetPolicyAllowed(false);
setOrgPolicy(null);
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred getting the policy from the organization, please try again later.',
});
}
}
}
async function updateAuthorizationPolicy() {
try {
setIsSaving(true);
await API.updateAuthorizationPolicy(selectedOrg!, orgPolicy!);
getAuthorizationPolicy();
// Update allowed actions and re-render button
authorizer.getAllowedActionsList(() => updateActionBtn.current!.reRender());
setIsSaving(false);
} catch (err: any) {
setIsSaving(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error: string | JSX.Element = compoundErrorMessage(err, 'An error occurred updating the policy');
error = (
<>
{error}. For more information please see the{' '}
<ExternalLink
href="https://github.com/artifacthub/hub/blob/master/docs/authorization.md"
className="text-primary fw-bold"
label="Open documentation"
>
documentation
</ExternalLink>
.
</>
);
if (err.kind === ErrorKind.Forbidden) {
error = 'You do not have permissions to update the policy from the organization.';
setUpdatePolicyAllowed(false);
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
async function fetchMembers() {
try {
const membersList: Member[] = await API.getAllOrganizationMembers(ctx.prefs.controlPanel.selectedOrg!);
setMembers(membersList.map((member: Member) => member.alias));
} catch (err: any) {
setMembers(undefined);
}
}
const onSaveAuthorizationPolicy = () => {
const policy = orgPolicy!.customPolicy || orgPolicy!.predefinedPolicy;
if (isNull(policy) || isUndefined(policy) || trim(policy) === '') {
setInvalidPolicy(true);
} else if (!isValidJSON(orgPolicy!.policyData || '')) {
setInvalidPolicyDataJSON(true);
} else {
checkPolicyChanges(updateAuthorizationPolicy, PolicyChangeAction.OnSavePolicy);
}
};
const onAuthorizationEnabledChange = () => {
let extraData = {};
const authorized = !orgPolicy!.authorizationEnabled;
const defaultPolicy = getPredefinedPolicy(DEFAULT_POLICY_NAME);
if (
authorized &&
(isNull(savedOrgPolicy!.customPolicy) || isUndefined(savedOrgPolicy!.customPolicy)) &&
(isNull(savedOrgPolicy!.predefinedPolicy) || isUndefined(savedOrgPolicy!.predefinedPolicy)) &&
!isUndefined(defaultPolicy)
) {
extraData = {
predefinedPolicy: defaultPolicy.name,
policyData: stringifyPolicyData(defaultPolicy.data),
};
}
const updatedOrgPolicy = {
...savedOrgPolicy!,
...extraData,
authorizationEnabled: authorized,
};
if (!authorized) {
checkPolicyChanges(() => setOrgPolicy(updatedOrgPolicy), PolicyChangeAction.OnDisableAuthorization);
} else {
setOrgPolicy(updatedOrgPolicy);
}
};
const onPredefinedPolicyChange = (e: ChangeEvent<HTMLSelectElement>) => {
e.preventDefault();
const activePredefinedPolicy = getPredefinedPolicy(e.target.value);
const updatedOrgPolicy = {
...orgPolicy!,
predefinedPolicy: e.target.value,
policyData: !isUndefined(activePredefinedPolicy) ? stringifyPolicyData(activePredefinedPolicy.data) : '',
};
checkPolicyChanges(() => setOrgPolicy(updatedOrgPolicy!), PolicyChangeAction.OnChangePredefinedPolicy);
};
const checkPolicyChanges = (onConfirmAction: () => void, action?: PolicyChangeAction) => {
const currentPredefinedPolicy =
orgPolicy && orgPolicy.predefinedPolicy ? getPredefinedPolicy(orgPolicy.predefinedPolicy) : undefined;
const lostData = checkUnsavedPolicyChanges(
savedOrgPolicy!,
orgPolicy!,
action,
currentPredefinedPolicy ? currentPredefinedPolicy.data : undefined
);
if (lostData.lostData) {
setConfirmationModal({
open: true,
message: lostData.message,
onConfirm: onConfirmAction,
});
} else {
onConfirmAction();
}
};
useEffect(() => {
if (selectedOrg) {
getAuthorizationPolicy();
fetchMembers();
}
}, [selectedOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */
useEffect(() => {
if (ctx.prefs.controlPanel.selectedOrg) {
if (selectedOrg !== ctx.prefs.controlPanel.selectedOrg) {
if (!checkIfUnsavedChanges()) {
setSelectedOrg(ctx.prefs.controlPanel.selectedOrg);
} else {
const warningPrompt = window.confirm(
'You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost.'
);
if (!warningPrompt) {
dispatch(updateOrg(selectedOrg!));
} else {
setSelectedOrg(ctx.prefs.controlPanel.selectedOrg);
}
}
}
}
}, [ctx.prefs.controlPanel.selectedOrg]); /* eslint-disable-line react-hooks/exhaustive-deps */
const onBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue =
'You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost.';
};
useEffect(() => {
if (checkIfUnsavedChanges()) {
window.addEventListener('beforeunload', onBeforeUnload);
} else {
window.removeEventListener('beforeunload', onBeforeUnload);
}
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);
};
}, [orgPolicy]); /* eslint-disable-line react-hooks/exhaustive-deps */
return (
<main role="main" className="p-0">
{(isUndefined(orgPolicy) || isLoading) && <Loading />}
<Prompt
when={!isNull(orgPolicy) && !isUndefined(orgPolicy) && !notGetPolicyAllowed && checkIfUnsavedChanges()}
message="You have some unsaved changes in your policy data. If you continue without saving, those changes will be lost."
/>
<div className={`h3 pb-2 border-bottom ${styles.title}`}>Authorization</div>
<div className="mt-4 mt-md-5" onClick={() => setApiError(null)}>
<p>
{siteName} allows you to setup fine-grained access control based on authorization policies. Authorization
polices are written in{' '}
<ExternalLink
href="https://www.openpolicyagent.org/docs/latest/#rego"
className="text-primary fw-bold"
label="Open rego documentation"
>
rego
</ExternalLink>{' '}
and they are evaluated using the{' '}
<ExternalLink
href="https://www.openpolicyagent.org"
className="text-primary fw-bold"
label="Open Open Policy Agent documentation"
>
Open Policy Agent
</ExternalLink>
. Depending on your requirements, you can use a predefined policy and only supply a data file, or you can
provide your custom policy for maximum flexibility. For more information please see the{' '}
<ExternalLink href="/docs/authorization" className="text-primary fw-bold" label="Open documentation">
documentation
</ExternalLink>
.
</p>
{(isNull(orgPolicy) || isUndefined(orgPolicy)) && notGetPolicyAllowed && (
<NoData>You are not allowed to manage this organization's authorization policy</NoData>
)}
{orgPolicy && (
<>
<div className="form-check form-switch mb-4">
<input
id="activeAuthorization"
type="checkbox"
className="form-check-input"
value="true"
role="switch"
onChange={onAuthorizationEnabledChange}
checked={orgPolicy.authorizationEnabled}
disabled={!updatePolicyAllowed}
/>
<label className="form-check-label" htmlFor="activeAuthorization">
Fine-grained access control
</label>
</div>
{orgPolicy.authorizationEnabled && (
<>
<label className={`form-label ${styles.label}`} htmlFor="payload">
<span className="fw-bold">Select authorization policy:</span>
</label>
<div className="d-flex flex-row mb-2">
{PAYLOAD_OPTION.map((item: Option) => {
const activeOption = !isNull(orgPolicy.predefinedPolicy) ? 'predefined' : 'custom';
return (
<div className="form-check me-4 mb-2" key={`payload_${item.name}`}>
<input
className="form-check-input"
type="radio"
id={item.name}
name="payload"
value={item.name}
checked={activeOption === item.name}
onChange={onPayloadChange}
disabled={!updatePolicyAllowed}
/>
<label className="form-check-label" htmlFor={item.name}>
{item.label}
</label>
</div>
);
})}
</div>
{orgPolicy.predefinedPolicy && (
<div className=" w-75 mb-4">
<select
className="form-select"
aria-label="org-select"
value={orgPolicy.predefinedPolicy || ''}
onChange={onPredefinedPolicyChange}
required={!isNull(orgPolicy.predefinedPolicy)}
disabled={!updatePolicyAllowed}
>
<option value="" disabled>
Select policy
</option>
{PREDEFINED_POLICIES.map((item: Option) => (
<option key={`policy_${item.name}`} value={item.name}>
{item.label}
</option>
))}
</select>
<div className={`invalid-feedback ${styles.fieldFeedback}`}>This field is required</div>
</div>
)}
<div className="d-flex flex-row align-self-stretch">
<div className="d-flex flex-column w-50 pe-2">
<div className="text-uppercase text-muted mb-2">Policy</div>
<div className="flex-grow-1">
<CodeEditor
mode="rego"
value={
orgPolicy.predefinedPolicy
? getPredefinedPolicy(orgPolicy.predefinedPolicy)!.policy
: orgPolicy.customPolicy
}
onChange={(value: string) => {
if (invalidPolicy) {
setInvalidPolicy(false);
}
setOrgPolicy({
...orgPolicy!,
customPolicy: value || null,
});
}}
disabled={orgPolicy.predefinedPolicy || !updatePolicyAllowed}
/>
{invalidPolicy && (
<small className="text-danger">
<span className="fw-bold">Error: </span> This field is required
</small>
)}
</div>
</div>
<div className="d-flex flex-column w-50 ps-2">
<div className="text-uppercase text-muted mb-2">Data</div>
<div className="flex-grow-1">
<CodeEditor
value={orgPolicy.policyData}
mode="javascript"
onChange={(value: string) => {
if (invalidPolicyDataJSON) {
setInvalidPolicyDataJSON(false);
}
setOrgPolicy({
...orgPolicy!,
policyData: value || null,
});
}}
disabled={!updatePolicyAllowed}
/>
{invalidPolicyDataJSON && (
<small className="text-danger">
<span className="fw-bold">Error: </span> Invalid JSON format
</small>
)}
</div>
</div>
</div>
</>
)}
<div className="d-flex flex-row mt-4">
{orgPolicy.authorizationEnabled && (
<button
type="button"
className="btn btn-sm btn-success"
onClick={triggerTestInRegoPlayground}
aria-label="Test in playground"
>
{isTesting ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Preparing Playground...</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<RiTestTubeFill className="me-2" /> <div>Test in Playground</div>
</div>
)}
</button>
)}
<div className="ms-auto">
<ActionBtn
ref={updateActionBtn}
className="btn btn-sm btn-outline-secondary"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onSaveAuthorizationPolicy();
}}
action={AuthorizerAction.UpdateAuthorizationPolicy}
disabled={isSaving}
label="Update authorization policy"
>
<>
{isSaving ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Saving</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<FaPencilAlt className="me-2" />
<div>Save</div>
</div>
)}
</>
</ActionBtn>
</div>
</div>
</>
)}
<Alert message={apiError} type="danger" onClose={() => setApiError(null)} />
</div>
{confirmationModal.open && (
<Modal
className={`d-inline-block ${styles.modal}`}
closeButton={
<>
<button
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setConfirmationModal({ open: false })}
aria-label="Cancel"
>
Cancel
</button>
<button
className="btn btn-sm btn-outline-secondary text-uppercase ms-3"
onClick={(e) => {
e.preventDefault();
confirmationModal.onConfirm!();
setConfirmationModal({ open: false });
}}
aria-label="Confirm"
>
Ok
</button>
</>
}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Confirm action</div>}
onClose={() => setConfirmationModal({ open: false })}
open
>
<div className="mt-3 mw-100 text-center">
<p>{confirmationModal.message!}</p>
</div>
</Modal>
)}
</main>
);
}
Example #6
Source File: Card.tsx From hub with Apache License 2.0 | 4 votes |
APIKeyCard = (props: Props) => {
const [isDeleting, setIsDeleting] = useState(false);
const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
const dropdownMenu = useRef(null);
const [deletionModalStatus, setDeletionModalStatus] = useState<boolean>(false);
const closeDropdown = () => {
setDropdownMenuStatus(false);
};
useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);
async function deleteAPIKey() {
try {
setIsDeleting(true);
await API.deleteAPIKey(props.apiKey.apiKeyId!);
setIsDeleting(false);
props.onSuccess();
} catch (err: any) {
setIsDeleting(false);
if (err.kind === ErrorKind.Unauthorized) {
props.onAuthError();
} else {
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred deleting the API key, please try again later.',
});
}
}
}
return (
<div className="col-12 col-xxl-6 py-sm-3 py-2 px-0 px-xxl-3" data-testid="APIKeyCard">
<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={`h5 mb-1 me-2 text-break ${styles.titleCard}`}>{props.apiKey.name}</div>
{deletionModalStatus && (
<Modal
className={`d-inline-block ${styles.modal}`}
closeButton={
<>
<button
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setDeletionModalStatus(false)}
aria-label="Cancel"
>
<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();
closeDropdown();
deleteAPIKey();
}}
disabled={isDeleting}
aria-label="Delete API key"
>
<div className="d-flex flex-row align-items-center text-uppercase">
{isDeleting ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Deleting...</span>
</>
) : (
<>
<FaTrashAlt className={`me-2 ${styles.btnDeleteIcon}`} />
<span>Delete</span>
</>
)}
</div>
</button>
</>
}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Delete API key</div>}
onClose={() => setDeletionModalStatus(false)}
open
>
<div className="mt-3 mw-100 text-center">
<p>Are you sure you want to remove this API key?</p>
</div>
</Modal>
)}
<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}`} />
<button
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
props.setModalStatus({
open: true,
apiKey: props.apiKey,
});
}}
aria-label="Open API key modal"
>
<div className="d-flex flex-row align-items-center">
<FaPencilAlt className={`me-2 ${styles.btnIcon}`} />
<span>Edit</span>
</div>
</button>
<button
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
setDeletionModalStatus(true);
}}
aria-label="Open deletion modal"
>
<div className="d-flex flex-row align-items-center">
<FaTrashAlt className={`me-2 ${styles.btnIcon}`} />
<span>Delete</span>
</div>
</button>
</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>
<div className="mt-2 d-flex flex-row align-items-baseline">
<div className="text-truncate">
<small className="text-muted text-uppercase me-1">API-KEY-ID: </small>
<small>{props.apiKey.apiKeyId}</small>
</div>
<div className={`ms-1 ${styles.copyBtn}`}>
<div className={`position-absolute ${styles.copyBtnWrapper}`}>
<ButtonCopyToClipboard
text={props.apiKey.apiKeyId!}
className="btn-link border-0 text-dark fw-bold"
label="Copy API key ID to clipboard"
/>
</div>
</div>
</div>
<div className="text-truncate">
<small className="text-muted text-uppercase me-1">Created at: </small>
<small>{moment.unix(props.apiKey.createdAt!).format('YYYY/MM/DD HH:mm:ss (Z)')}</small>
</div>
</div>
</div>
</div>
);
}
Example #7
Source File: Modal.tsx From hub with Apache License 2.0 | 4 votes |
APIKeyModal = (props: Props) => {
const form = useRef<HTMLFormElement>(null);
const nameInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [apiKey, setApiKey] = useState<APIKey | undefined>(props.apiKey);
const [apiError, setApiError] = useState<string | null>(null);
const [apiKeyCode, setApiKeyCode] = useState<APIKeyCode | undefined>(undefined);
// Clean API error when form is focused after validation
const cleanApiError = () => {
if (!isNull(apiError)) {
setApiError(null);
}
};
const onCloseModal = () => {
setApiKey(undefined);
setApiKeyCode(undefined);
setIsValidated(false);
setApiError(null);
props.onClose();
};
async function handleAPIKey(name: string) {
try {
if (props.apiKey) {
await API.updateAPIKey(props.apiKey.apiKeyId!, name);
} else {
setApiKeyCode(await API.addAPIKey(name));
}
if (props.onSuccess) {
props.onSuccess();
}
setIsSending(false);
// Modal is closed only when updating API key
if (props.apiKey) {
onCloseModal();
}
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
setApiError(
`An error occurred ${isUndefined(props.apiKey) ? 'adding' : 'updating'} the API key, please try again later.`
);
} else {
props.onAuthError();
}
}
}
const submitForm = () => {
cleanApiError();
setIsSending(true);
if (form.current) {
const { isValid, apiKey } = validateForm(form.current);
if (isValid && apiKey) {
handleAPIKey(apiKey.name);
} else {
setIsSending(false);
}
}
};
const validateForm = (form: HTMLFormElement): FormValidation => {
let apiKey: APIKey | null = null;
const formData = new FormData(form);
const isValid = form.checkValidity();
if (isValid) {
apiKey = {
name: formData.get('name') as string,
};
}
setIsValidated(true);
return { isValid, apiKey };
};
const handleOnReturnKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter' && form) {
event.preventDefault();
event.stopPropagation();
submitForm();
}
};
useEffect(() => {
async function getAPIKey() {
try {
const currentAPIKey = await API.getAPIKey(props.apiKey!.apiKeyId!);
setApiKey(currentAPIKey);
nameInput.current!.updateValue(currentAPIKey.name);
} catch (err: any) {
if (err.kind === ErrorKind.Unauthorized) {
props.onAuthError();
}
}
}
if (props.apiKey && isUndefined(apiKey)) {
getAPIKey();
}
}, [apiKey, props, props.apiKey]);
const sendBtn = (
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label={`${props.apiKey ? 'Update' : 'Add'} API key`}
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">{`${props.apiKey ? 'Updating' : 'Adding'} API key`}</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
{isUndefined(props.apiKey) ? (
<>
<MdAddCircle className="me-2" />
<div>Add</div>
</>
) : (
<>
<FaPencilAlt className="me-2" />
<div>Update</div>
</>
)}
</div>
)}
</button>
);
return (
<Modal
header={
<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>{`${props.apiKey ? 'Update' : 'Add'} API key`}</div>
}
open={props.open}
modalClassName={styles.modal}
closeButton={isUndefined(apiKeyCode) ? sendBtn : undefined}
onClose={onCloseModal}
error={apiError}
cleanError={cleanApiError}
>
<div className={`w-100 ${styles.contentWrapper}`}>
{apiKeyCode ? (
<>
<div className="d-flex justify-content-between mb-2">
<SmallTitle text="API-KEY-ID" />
<div>
<ButtonCopyToClipboard text={apiKeyCode.apiKeyId} label="Copy API key ID to clipboard" />
</div>
</div>
<SyntaxHighlighter
language="bash"
style={docco}
customStyle={{
backgroundColor: 'var(--color-1-10)',
}}
>
{apiKeyCode.apiKeyId}
</SyntaxHighlighter>
<div className="d-flex justify-content-between mb-2">
<SmallTitle text="API-KEY-SECRET" />
<div>
<ButtonCopyToClipboard text={apiKeyCode.secret} label="Copy API key secret to clipboard" />
</div>
</div>
<SyntaxHighlighter
language="bash"
style={docco}
customStyle={{
backgroundColor: 'var(--color-1-10)',
}}
>
{apiKeyCode.secret}
</SyntaxHighlighter>
<small className="text-muted">
These are the credentials you will need to provide when making requests to the API. Please, copy and store
them in a safe place.{' '}
<b>
<u>You will not be able to see the secret again when you close this window.</u>
</b>{' '}
For more information please see the authorize section in the{' '}
<ExternalLink className="text-primary fw-bold" href="/docs/api" label="Open documentation">
API docs
</ExternalLink>
.
</small>
<div className={`alert alert-warning mt-4 mb-2 ${styles.alert}`}>
<span className="fw-bold text-uppercase">Important:</span> the API key you've just generated can be used
to perform <u className="fw-bold">ANY</u> operation you can, so please store it safely and don't share it
with others.
</div>
</>
) : (
<form
data-testid="apiKeyForm"
ref={form}
className={classnames('w-100 mt-3', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
onFocus={cleanApiError}
autoComplete="on"
noValidate
>
<InputField
ref={nameInput}
type="text"
label="Name"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="name"
value={isUndefined(apiKey) ? '' : apiKey.name}
invalidText={{
default: 'This field is required',
}}
autoComplete="off"
onKeyDown={handleOnReturnKeyDown}
required
/>
</form>
)}
</div>
</Modal>
);
}
Example #8
Source File: UpdatePassword.tsx From hub with Apache License 2.0 | 4 votes |
UpdatePassword = () => {
const form = useRef<HTMLFormElement>(null);
const oldPasswordInput = useRef<RefInputField>(null);
const passwordInput = useRef<RefInputField>(null);
const repeatPasswordInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [password, setPassword] = useState<Password>({ value: '', isValid: false });
const [isValidated, setIsValidated] = useState(false);
const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword({ value: e.target.value, isValid: e.currentTarget.checkValidity() });
};
async function updatePassword(oldPassword: string, newPassword: string) {
try {
setIsSending(true);
await API.updatePassword(oldPassword, newPassword);
cleanForm();
setIsSending(false);
setIsValidated(false);
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(err, 'An error occurred updating your password');
alertDispatcher.postAlert({
type: 'danger',
message: error,
});
} else {
alertDispatcher.postAlert({
type: 'danger',
message:
'An error occurred updating your password, please make sure you have entered your old password correctly',
});
}
}
}
const cleanForm = () => {
oldPasswordInput.current!.reset();
passwordInput.current!.reset();
repeatPasswordInput.current!.reset();
};
const submitForm = () => {
setIsSending(true);
if (form.current) {
validateForm(form.current).then((validation: FormValidation) => {
if (validation.isValid) {
updatePassword(validation.oldPassword!, validation.newPassword!);
} else {
setIsSending(false);
}
});
}
};
const validateForm = async (form: HTMLFormElement): Promise<FormValidation> => {
let newPassword: string | null = null;
let oldPassword: string | null = null;
return validateAllFields().then((isValid: boolean) => {
if (isValid) {
const formData = new FormData(form);
newPassword = formData.get('password') as string;
oldPassword = formData.get('oldPassword') as string;
}
setIsValidated(true);
return { isValid, newPassword, oldPassword };
});
};
const validateAllFields = async (): Promise<boolean> => {
return Promise.all([
oldPasswordInput.current!.checkIsValid(),
passwordInput.current!.checkIsValid(),
repeatPasswordInput.current!.checkIsValid(),
]).then((res: boolean[]) => {
return every(res, (isValid: boolean) => isValid);
});
};
return (
<form
data-testid="updatePasswordForm"
ref={form}
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
autoComplete="on"
noValidate
>
<InputField
ref={oldPasswordInput}
type="password"
label="Old password"
name="oldPassword"
invalidText={{
default: 'This field is required',
}}
autoComplete="password"
validateOnBlur
required
/>
<InputField
ref={passwordInput}
type="password"
label="New password"
name="password"
minLength={6}
invalidText={{
default: 'This field is required',
customError: 'Insecure password',
}}
onChange={onPasswordChange}
autoComplete="new-password"
checkPasswordStrength
validateOnChange
validateOnBlur
required
/>
<InputField
ref={repeatPasswordInput}
type="password"
label="Confirm new password"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="confirmPassword"
pattern={password.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}
invalidText={{
default: 'This field is required',
patternMismatch: "Passwords don't match",
}}
autoComplete="new-password"
validateOnBlur={password.isValid}
required
/>
<div className="mt-4 mb-2">
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label="Update password"
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Changing password</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<FaPencilAlt className="me-2" />
<div>Change</div>
</div>
)}
</button>
</div>
</form>
);
}
Example #9
Source File: UpdateProfile.tsx From hub with Apache License 2.0 | 4 votes |
UpdateProfile = (props: Props) => {
const { dispatch } = useContext(AppCtx);
const form = useRef<HTMLFormElement>(null);
const usernameInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [profile, setProfile] = useState<Profile | null | undefined>(props.profile);
const [imageId, setImageId] = useState<string | undefined>(
props.profile && props.profile.profileImageId ? props.profile.profileImageId : undefined
);
useEffect(() => {
setProfile(props.profile);
}, [props.profile]);
async function updateProfile(user: UserFullName) {
try {
setIsSending(true);
const formattedUser = { ...user };
await API.updateUserProfile(user);
dispatch(updateUser(formattedUser));
setIsSending(false);
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(err, 'An error occurred updating your profile');
alertDispatcher.postAlert({
type: 'danger',
message: error,
});
} else {
props.onAuthError();
}
}
}
const submitForm = () => {
setIsSending(true);
if (form.current) {
validateForm(form.current).then((validation: FormValidation) => {
if (validation.isValid && !isNull(validation.user)) {
updateProfile(validation.user);
} else {
setIsSending(false);
}
});
}
};
const validateForm = (form: HTMLFormElement): Promise<FormValidation> => {
let user: User | null = null;
return validateAllFields().then((isValid: boolean) => {
if (isValid) {
const formData = new FormData(form);
user = {
alias: formData.get('alias') as string,
};
if (formData.get('firstName') !== '') {
user['firstName'] = formData.get('firstName') as string;
}
if (formData.get('lastName') !== '') {
user['lastName'] = formData.get('lastName') as string;
}
if (!isUndefined(imageId)) {
user['profileImageId'] = imageId;
}
}
setIsValidated(true);
return { isValid, user };
});
};
const validateAllFields = async (): Promise<boolean> => {
return Promise.all([usernameInput.current!.checkIsValid()]).then((res: boolean[]) => {
return every(res, (isValid: boolean) => isValid);
});
};
return (
<form
data-testid="updateProfileForm"
ref={form}
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
autoComplete="on"
noValidate
>
<InputFileField
name="image"
label="Profile image"
labelLegend={<small className="ms-1 fst-italic">(Click on the image to update)</small>}
value={imageId}
onImageChange={(imageId: string) => setImageId(imageId)}
onAuthError={props.onAuthError}
/>
<InputField
type="email"
label="Email"
name="email"
value={!isUndefined(profile) && !isNull(profile) ? profile.email : ''}
readOnly
/>
<InputField
ref={usernameInput}
type="text"
label="Username"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="alias"
value={!isUndefined(profile) && !isNull(profile) ? profile.alias : ''}
invalidText={{
default: 'This field is required',
customError: 'Username not available',
}}
checkAvailability={{
isAvailable: true,
resourceKind: ResourceKind.userAlias,
excluded: !isUndefined(profile) && !isNull(profile) ? [profile.alias] : [],
}}
validateOnBlur
autoComplete="username"
required
/>
<InputField
type="text"
label="First Name"
name="firstName"
autoComplete="given-name"
value={!isUndefined(profile) && !isNull(profile) ? profile.firstName : ''}
/>
<InputField
type="text"
label="Last Name"
name="lastName"
autoComplete="family-name"
value={!isUndefined(profile) && !isNull(profile) ? profile.lastName : ''}
/>
<div className="mt-4">
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label="Update profile"
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Updating profile</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<FaPencilAlt className="me-2" />
<div>Update</div>
</div>
)}
</button>
</div>
</form>
);
}
Example #10
Source File: Card.tsx From hub with Apache License 2.0 | 4 votes |
WebhookCard = (props: Props) => {
const { ctx } = useContext(AppCtx);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
const [dropdownMenuStatus, setDropdownMenuStatus] = useState<boolean>(false);
const dropdownMenu = useRef(null);
const [deletionModalStatus, setDeletionModalStatus] = useState<boolean>(false);
const closeDropdown = () => {
setDropdownMenuStatus(false);
};
useOutsideClick([dropdownMenu], dropdownMenuStatus, closeDropdown);
async function deleteWebhook() {
try {
setIsDeleting(true);
await API.deleteWebhook(props.webhook.webhookId!, ctx.prefs.controlPanel.selectedOrg);
setIsDeleting(false);
props.onDeletion();
} catch (err: any) {
setIsDeleting(false);
if (err.kind === ErrorKind.Unauthorized) {
props.onAuthError();
} else {
alertDispatcher.postAlert({
type: 'danger',
message: 'An error occurred deleting the webhook, please try again later.',
});
}
}
}
return (
<div className="col-12 col-xxl-6 py-sm-3 py-2 px-0 px-xxl-3" role="listitem">
<div className={`card cardWithHover w-100 h-100 mw-100 bg-white ${styles.card}`}>
<div className="card-body position-relative">
<div className="d-flex flex-row">
<div className="h5 card-title mb-3 me-3 lh-1 text-break">
<div className="d-flex flex-row align-items-start">
<div>{props.webhook.name}</div>
{props.webhook.active ? (
<span
className={`ms-3 mt-1 fw-bold badge rounded-pill border border-success text-success text-uppercase ${styles.badge}`}
>
Active
</span>
) : (
<span
className={`ms-3 mt-1 fw-bold badge rounded-pill border border-dark text-dark text-uppercase ${styles.badge} ${styles.inactiveBadge}`}
>
Inactive
</span>
)}
</div>
</div>
{deletionModalStatus && (
<Modal
className={`d-inline-block ${styles.modal}`}
closeButton={
<>
<button
className="btn btn-sm btn-outline-secondary text-uppercase"
onClick={() => setDeletionModalStatus(false)}
aria-label="Close deletion 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();
deleteWebhook();
}}
disabled={isDeleting}
aria-label="Delete webhook"
>
<div className="d-flex flex-row align-items-center text-uppercase">
{isDeleting ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">Deleting...</span>
</>
) : (
<>
<FaTrashAlt className={`me-2 ${styles.btnDeleteIcon}`} />
<span>Delete</span>
</>
)}
</div>
</button>
</>
}
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Delete webhook</div>}
onClose={() => setDeletionModalStatus(false)}
open
>
<div className="mt-3 mw-100 text-center">
<p>Are you sure you want to delete this webhook?</p>
</div>
</Modal>
)}
<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}`} />
<button
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
props.onEdition();
}}
aria-label="Edit webhook"
>
<div className="d-flex flex-row align-items-center">
<FaPencilAlt className={`me-2 ${styles.btnIcon}`} />
<span>Edit</span>
</div>
</button>
<button
className="dropdown-item btn btn-sm rounded-0 text-dark"
onClick={(e: ReactMouseEvent<HTMLButtonElement>) => {
e.preventDefault();
closeDropdown();
setDeletionModalStatus(true);
}}
aria-label="Open deletion webhook modal"
>
<div className="d-flex flex-row align-items-center">
<FaTrashAlt className={`me-2 ${styles.btnIcon}`} />
<span>Delete</span>
</div>
</button>
</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>
<div className="d-flex flex-column">
<div className="card-subtitle d-flex flex-column mw-100 mt-1">
<p className="card-text">{props.webhook.description}</p>
</div>
<div className="text-truncate">
<small className="text-muted text-uppercase me-2">Url:</small>
<small>{props.webhook.url}</small>
</div>
<div className="d-flex flex-row justify-content-between align-items-baseline">
{props.webhook.lastNotifications && (
<div className="d-none d-md-inline mt-2">
<LastNotificationsModal notifications={props.webhook.lastNotifications} />
</div>
)}
{(isUndefined(props.webhook.packages) || props.webhook.packages.length === 0) && (
<div className="ms-auto mt-2">
<ElementWithTooltip
element={
<span
className={`d-flex flex-row align-items-center badge bg-warning rounded-pill ${styles.badgeNoPackages}`}
>
<TiWarningOutline />
<span className="ms-1">No packages</span>
</span>
}
tooltipMessage="This webhook is not associated to any packages."
active
visibleTooltip
/>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
Example #11
Source File: Form.tsx From hub with Apache License 2.0 | 4 votes |
WebhookForm = (props: Props) => {
const { ctx } = useContext(AppCtx);
const form = useRef<HTMLFormElement>(null);
const urlInput = useRef<RefInputField>(null);
const contentTypeInput = useRef<RefInputField>(null);
const [isSending, setIsSending] = useState(false);
const [isValidated, setIsValidated] = useState(false);
const [apiError, setApiError] = useState<string | null>(null);
const [selectedPackages, setSelectedPackages] = useState<Package[]>(
!isUndefined(props.webhook) && props.webhook.packages ? props.webhook.packages : []
);
const [eventKinds, setEventKinds] = useState<EventKind[]>(
!isUndefined(props.webhook) ? props.webhook.eventKinds : [EventKind.NewPackageRelease]
);
const [isActive, setIsActive] = useState<boolean>(!isUndefined(props.webhook) ? props.webhook.active : true);
const [contentType, setContentType] = useState<string>(
!isUndefined(props.webhook) && props.webhook.contentType ? props.webhook.contentType : ''
);
const [template, setTemplate] = useState<string>(
!isUndefined(props.webhook) && props.webhook.template ? props.webhook.template : ''
);
const [isAvailableTest, setIsAvailableTest] = useState<boolean>(false);
const [currentTestWebhook, setCurrentTestWebhook] = useState<TestWebhook | null>(null);
const [isTestSent, setIsTestSent] = useState<boolean>(false);
const [isSendingTest, setIsSendingTest] = useState<boolean>(false);
const getPayloadKind = (): PayloadKind => {
let currentPayloadKind: PayloadKind = DEFAULT_PAYLOAD_KIND;
if (!isUndefined(props.webhook) && props.webhook.contentType && props.webhook.template) {
currentPayloadKind = PayloadKind.custom;
}
return currentPayloadKind;
};
const [payloadKind, setPayloadKind] = useState<PayloadKind>(getPayloadKind());
const onCloseForm = () => {
props.onClose();
};
const onContentTypeChange = (e: ChangeEvent<HTMLInputElement>) => {
setContentType(e.target.value);
};
async function handleWebhook(webhook: Webhook) {
try {
setIsSending(true);
if (isUndefined(props.webhook)) {
await API.addWebhook(webhook, ctx.prefs.controlPanel.selectedOrg!);
} else {
await API.updateWebhook(webhook, ctx.prefs.controlPanel.selectedOrg!);
}
setIsSending(false);
props.onSuccess();
onCloseForm();
} catch (err: any) {
setIsSending(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(
err,
`An error occurred ${isUndefined(props.webhook) ? 'adding' : 'updating'} the webhook`
);
if (!isUndefined(props.webhook) && err.kind === ErrorKind.Forbidden) {
error = `You do not have permissions to ${isUndefined(props.webhook) ? 'add' : 'update'} the webhook ${
isUndefined(props.webhook) ? 'to' : 'from'
} the organization.`;
}
setApiError(error);
} else {
props.onAuthError();
}
}
}
async function triggerWebhookTest(webhook: TestWebhook) {
try {
setIsSendingTest(true);
setIsTestSent(false);
await API.triggerWebhookTest(webhook);
setIsTestSent(true);
setIsSendingTest(false);
} catch (err: any) {
setIsSendingTest(false);
if (err.kind !== ErrorKind.Unauthorized) {
let error = compoundErrorMessage(err, `An error occurred testing the webhook`);
setApiError(error);
} else {
props.onAuthError();
}
}
}
const triggerTest = () => {
if (!isNull(currentTestWebhook)) {
cleanApiError();
triggerWebhookTest(currentTestWebhook);
}
};
const submitForm = () => {
if (form.current) {
cleanApiError();
const { isValid, webhook } = validateForm(form.current);
if (isValid && !isNull(webhook)) {
handleWebhook(webhook);
}
}
};
const validateForm = (form: HTMLFormElement): FormValidation => {
let webhook: Webhook | null = null;
const formData = new FormData(form);
const isValid = form.checkValidity() && selectedPackages.length > 0;
if (isValid) {
webhook = {
name: formData.get('name') as string,
url: formData.get('url') as string,
secret: formData.get('secret') as string,
description: formData.get('description') as string,
eventKinds: eventKinds,
active: isActive,
packages: selectedPackages,
};
if (payloadKind === PayloadKind.custom) {
webhook = {
...webhook,
template: template,
contentType: contentType,
};
}
if (props.webhook) {
webhook = {
...webhook,
webhookId: props.webhook.webhookId,
};
}
}
setIsValidated(true);
return { isValid, webhook };
};
const addPackage = (packageItem: Package) => {
const packagesList = [...selectedPackages];
packagesList.push(packageItem);
setSelectedPackages(packagesList);
};
const deletePackage = (packageId: string) => {
const packagesList = selectedPackages.filter((item: Package) => item.packageId !== packageId);
setSelectedPackages(packagesList);
};
const getPackagesIds = (): string[] => {
return selectedPackages.map((item: Package) => item.packageId);
};
const updateEventKindList = (eventKind: EventKind) => {
let updatedEventKinds: EventKind[] = [...eventKinds];
if (eventKinds.includes(eventKind)) {
// At least event kind must be selected
if (updatedEventKinds.length > 1) {
updatedEventKinds = eventKinds.filter((kind: EventKind) => kind !== eventKind);
}
} else {
updatedEventKinds.push(eventKind);
}
setEventKinds(updatedEventKinds);
};
const cleanApiError = () => {
if (!isNull(apiError)) {
setApiError(null);
}
};
const updateTemplate = (e: ChangeEvent<HTMLTextAreaElement>) => {
setTemplate(e.target.value);
checkTestAvailability();
};
const checkTestAvailability = () => {
const formData = new FormData(form.current!);
let webhook: TestWebhook = {
url: formData.get('url') as string,
eventKinds: eventKinds,
};
if (payloadKind === PayloadKind.custom) {
webhook = {
...webhook,
template: template,
contentType: contentType,
};
}
const isFilled = Object.values(webhook).every((x) => x !== null && x !== '');
if (urlInput.current!.checkValidity() && isFilled) {
setCurrentTestWebhook(webhook);
setIsAvailableTest(true);
} else {
setCurrentTestWebhook(null);
setIsAvailableTest(false);
}
};
useEffect(() => {
checkTestAvailability();
}, []); /* eslint-disable-line react-hooks/exhaustive-deps */
const getPublisher = (pkg: Package): JSX.Element => {
return (
<>
{pkg.repository.userAlias || pkg.repository.organizationDisplayName || pkg.repository.organizationName}
<small className="ms-2">
(<span className={`text-uppercase text-muted d-none d-sm-inline ${styles.legend}`}>Repo: </span>
<span className="text-dark">{pkg.repository.displayName || pkg.repository.name}</span>)
</small>
</>
);
};
return (
<div>
<div className="mb-4 pb-2 border-bottom">
<button
className={`btn btn-link text-dark btn-sm ps-0 d-flex align-items-center ${styles.link}`}
onClick={onCloseForm}
aria-label="Back to webhooks list"
>
<IoIosArrowBack className="me-2" />
Back to webhooks list
</button>
</div>
<div className="mt-2">
<form
ref={form}
data-testid="webhookForm"
className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
onClick={() => setApiError(null)}
autoComplete="off"
noValidate
>
<div className="d-flex">
<div className="col-md-8">
<InputField
type="text"
label="Name"
labelLegend={<small className="ms-1 fst-italic">(Required)</small>}
name="name"
value={!isUndefined(props.webhook) ? props.webhook.name : ''}
invalidText={{
default: 'This field is required',
}}
validateOnBlur
required
/>
</div>
</div>
<div className="d-flex">
<div className="col-md-8">
<InputField
type="text"
label="Description"
name="description"
value={!isUndefined(props.webhook) ? props.webhook.description : ''}
/>
</div>
</div>
<div>
<label className={`form-label fw-bold ${styles.label}`} htmlFor="url">
Url<small className="ms-1 fst-italic">(Required)</small>
</label>
<div className="form-text text-muted mb-2 mt-0">
A POST request will be sent to the provided URL when any of the events selected in the triggers section
happens.
</div>
<div className="d-flex">
<div className="col-md-8">
<InputField
ref={urlInput}
type="url"
name="url"
value={!isUndefined(props.webhook) ? props.webhook.url : ''}
invalidText={{
default: 'This field is required',
typeMismatch: 'Please enter a valid url',
}}
onChange={checkTestAvailability}
validateOnBlur
required
/>
</div>
</div>
</div>
<div>
<label className={`form-label fw-bold ${styles.label}`} htmlFor="secret">
Secret
</label>
<div className="form-text text-muted mb-2 mt-0">
If you provide a secret, we'll send it to you in the <span className="fw-bold">X-ArtifactHub-Secret</span>{' '}
header on each request. This will allow you to validate that the request comes from ArtifactHub.
</div>
<div className="d-flex">
<div className="col-md-8">
<InputField type="text" name="secret" value={!isUndefined(props.webhook) ? props.webhook.secret : ''} />
</div>
</div>
</div>
<div className="mb-3">
<div className="form-check form-switch ps-0">
<label htmlFor="active" className={`form-check-label fw-bold ${styles.label}`}>
Active
</label>
<input
id="active"
type="checkbox"
role="switch"
className={`position-absolute ms-2 form-check-input ${styles.checkbox}`}
value="true"
onChange={() => setIsActive(!isActive)}
checked={isActive}
/>
</div>
<div className="form-text text-muted mt-2">
This flag indicates if the webhook is active or not. Inactive webhooks will not receive notifications.
</div>
</div>
<div className="h4 pb-2 mt-4 mt-md-5 mb-4 border-bottom">Triggers</div>
<div className="my-4">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="kind" id="events-group">
Events
</label>
<div role="group" aria-labelledby="events-group">
{PACKAGE_SUBSCRIPTIONS_LIST.map((subs: SubscriptionItem) => {
return (
<CheckBox
key={`check_${subs.kind}`}
name="eventKind"
value={subs.kind.toString()}
device="all"
label={subs.title}
checked={eventKinds.includes(subs.kind)}
onChange={() => {
updateEventKindList(subs.kind);
checkTestAvailability();
}}
/>
);
})}
</div>
</div>
<div className="mb-4">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="packages" id="webhook-pkg-list">
Packages<small className="ms-1 fst-italic">(Required)</small>
</label>
<div className="form-text text-muted mb-4 mt-0">
When the events selected happen for any of the packages you've chosen, a notification will be triggered
and the configured url will be called. At least one package must be selected.
</div>
<div className="mb-3 row">
<div className="col-12 col-xxl-10 col-xxxl-8">
<SearchPackages disabledPackages={getPackagesIds()} onSelection={addPackage} label="webhook-pkg-list" />
</div>
</div>
{isValidated && selectedPackages.length === 0 && (
<div className="invalid-feedback mt-0 d-block">At least one package has to be selected</div>
)}
{selectedPackages.length > 0 && (
<div className="row">
<div className="col-12 col-xxl-10 col-xxxl-8">
<table className={`table table-hover table-sm border transparentBorder text-break ${styles.table}`}>
<thead>
<tr className={styles.tableTitle}>
<th scope="col" className={`align-middle d-none d-sm-table-cell ${styles.fitCell}`}></th>
<th scope="col" className={`align-middle ${styles.packageCell}`}>
Package
</th>
<th scope="col" className="align-middle w-50 d-none d-sm-table-cell">
Publisher
</th>
<th scope="col" className={`align-middle ${styles.fitCell}`}></th>
</tr>
</thead>
<tbody className={styles.body}>
{selectedPackages.map((item: Package) => (
<tr key={`subs_${item.packageId}`} data-testid="packageTableCell">
<td className="align-middle text-center d-none d-sm-table-cell">
<RepositoryIcon kind={item.repository.kind} className={`${styles.icon} h-auto mx-2`} />
</td>
<td className="align-middle">
<div className="d-flex flex-row align-items-center">
<div
className={`d-flex align-items-center justify-content-center overflow-hidden p-1 rounded-circle border border-2 bg-white ${styles.imageWrapper} imageWrapper`}
>
<Image
alt={item.displayName || item.name}
imageId={item.logoImageId}
className="mw-100 mh-100 fs-4"
kind={item.repository.kind}
/>
</div>
<div className={`ms-2 text-dark ${styles.cellWrapper}`}>
<div className="text-truncate">
{item.displayName || item.name}
<span className={`d-inline d-sm-none ${styles.legend}`}>
<span className="mx-2">/</span>
{getPublisher(item)}
</span>
</div>
</div>
</div>
</td>
<td className="align-middle position-relative text-dark d-none d-sm-table-cell">
<div className={`d-table w-100 ${styles.cellWrapper}`}>
<div className="text-truncate">{getPublisher(item)}</div>
</div>
</td>
<td className="align-middle">
<button
className={`btn btn-link btn-sm mx-2 ${styles.closeBtn}`}
type="button"
onClick={(event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
deletePackage(item.packageId);
}}
aria-label="Delete package from webhook"
>
<MdClose className="text-danger fs-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
<div className="h4 pb-2 mt-4 mt-md-5 mb-4 border-bottom">Payload</div>
<div className="d-flex flex-row mb-3">
{PAYLOAD_KINDS_LIST.map((item: PayloadKindsItem) => {
return (
<div className="form-check me-4" key={`payload_${item.kind}`}>
<input
className="form-check-input"
type="radio"
id={`payload_${item.kind}`}
name="payloadKind"
value={item.name}
checked={payloadKind === item.kind}
onChange={() => {
setPayloadKind(item.kind);
setIsValidated(false);
checkTestAvailability();
}}
/>
<label className="form-check-label" htmlFor={`payload_${item.kind}`}>
{item.title}
</label>
</div>
);
})}
</div>
{payloadKind === PayloadKind.custom && (
<div className="lh-base">
<div className="form-text text-muted mb-3">
It's possible to customize the payload used to notify your service. This may help integrating
ArtifactHub webhooks with other services without requiring you to write any code. To integrate
ArtifactHub webhooks with Slack, for example, you could use a custom payload using the following
template:
<div className="my-3 w-100">
<div
className={`alert alert-light text-nowrap ${styles.codeWrapper}`}
role="alert"
aria-live="off"
aria-atomic="true"
>
{'{'}
<br />
<span className="ms-3">
{`"text": "Package`} <span className="fw-bold">{`{{ .Package.Name }}`}</span> {`version`}{' '}
<span className="fw-bold">{`{{ .Package.Version }}`}</span> released!{' '}
<span className="fw-bold">{`{{ .Package.URL }}`}</span>
{`"`}
<br />
{'}'}
</span>
</div>
</div>
</div>
</div>
)}
<div className="d-flex">
<div className="col-md-8">
<InputField
ref={contentTypeInput}
type="text"
label="Request Content-Type"
name="contentType"
value={contentType}
placeholder={payloadKind === PayloadKind.default ? 'application/cloudevents+json' : 'application/json'}
disabled={payloadKind === PayloadKind.default}
required={payloadKind !== PayloadKind.default}
invalidText={{
default: 'This field is required',
}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
onContentTypeChange(e);
checkTestAvailability();
}}
/>
</div>
</div>
<div className=" mb-4">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="template">
Template
</label>
{payloadKind === PayloadKind.custom && (
<div className="form-text text-muted mb-4 mt-0">
Custom payloads are generated using{' '}
<ExternalLink
href="https://golang.org/pkg/text/template/"
className="fw-bold text-dark"
label="Open Go templates documentation"
>
Go templates
</ExternalLink>
. Below you will find a list of the variables available for use in your template.
</div>
)}
<div className="row">
<div className="col col-xxl-10 col-xxxl-8">
<AutoresizeTextarea
name="template"
value={payloadKind === PayloadKind.default ? DEFAULT_PAYLOAD_TEMPLATE : template}
disabled={payloadKind === PayloadKind.default}
required={payloadKind !== PayloadKind.default}
invalidText="This field is required"
minRows={6}
onChange={updateTemplate}
/>
</div>
</div>
</div>
<div className="mb-3">
<label className={`form-label fw-bold ${styles.label}`} htmlFor="template">
Variables reference
</label>
<div className="row">
<div className="col col-xxxl-8 overflow-auto">
<small className={`text-muted ${styles.tableWrapper}`}>
<table className={`table table-sm border ${styles.variablesTable}`}>
<tbody>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .BaseURL }}`}</span>
</th>
<td>Artifact Hub deployment base url.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Event.ID }}`}</span>
</th>
<td>Id of the event triggering the notification.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Event.Kind }}`}</span>
</th>
<td>
Kind of the event triggering notification. Possible values are{' '}
<span className="fw-bold">package.new-release</span> and{' '}
<span className="fw-bold">package.security-alert</span>.
</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Name }}`}</span>
</th>
<td>Name of the package.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Version }}`}</span>
</th>
<td>Version of the new release.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.URL }}`}</span>
</th>
<td>ArtifactHub URL of the package.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes }}`}</span>
</th>
<td>List of changes this package version introduces.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes[i].Kind }}`}</span>
</th>
<td>
Kind of the change. Possible values are <span className="fw-bold">added</span>,{' '}
<span className="fw-bold">changed</span>, <span className="fw-bold">deprecated</span>,{' '}
<span className="fw-bold">removed</span>, <span className="fw-bold">fixed</span> and{' '}
<span className="fw-bold">security</span>. When the change kind is not provided, the value
will be empty.
</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes[i].Description }}`}</span>
</th>
<td>Brief text explaining the change.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes[i].Links }}`}</span>
</th>
<td>List of links related to the change.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes[i].Links[i].Name }}`}</span>
</th>
<td>Name of the link.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Changes[i].Links[i].URL }}`}</span>
</th>
<td>Url of the link.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.ContainsSecurityUpdates }}`}</span>
</th>
<td>Boolean flag that indicates whether this package contains security updates or not.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Prerelease }}`}</span>
</th>
<td>Boolean flag that indicates whether this package version is a pre-release or not.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Repository.Kind }}`}</span>
</th>
<td>
Kind of the repository associated with the notification. Possible values are{' '}
<span className="fw-bold">falco</span>, <span className="fw-bold">helm</span>,{' '}
<span className="fw-bold">olm</span> and <span className="fw-bold">opa</span>.
</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Repository.Name }}`}</span>
</th>
<td>Name of the repository.</td>
</tr>
<tr>
<th scope="row">
<span className="text-nowrap">{`{{ .Package.Repository.Publisher }}`}</span>
</th>
<td>
Publisher of the repository. If the owner is a user it'll be the user alias. If it's an
organization, it'll be the organization name.
</td>
</tr>
</tbody>
</table>
</small>
</div>
</div>
</div>
<div className={`mt-4 mt-md-5 ${styles.btnWrapper}`}>
<div className="d-flex flex-row justify-content-between">
<div className="d-flex flex-row align-items-center me-3">
<button
type="button"
className="btn btn-sm btn-success"
onClick={triggerTest}
disabled={!isAvailableTest || isSendingTest}
aria-label="Test webhook"
>
{isSendingTest ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">
Testing <span className="d-none d-md-inline"> webhook</span>
</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
<RiTestTubeFill className="me-2" />{' '}
<div>
Test <span className="d-none d-sm-inline-block">webhook</span>
</div>
</div>
)}
</button>
{isTestSent && (
<span className="text-success ms-2" data-testid="testWebhookTick">
<FaCheck />
</span>
)}
</div>
<div className="ms-auto">
<button
type="button"
className="btn btn-sm btn-outline-secondary me-3"
onClick={onCloseForm}
aria-label="Cancel"
>
<div className="d-flex flex-row align-items-center text-uppercase">
<MdClose className="me-2" />
<div>Cancel</div>
</div>
</button>
<button
className="btn btn-sm btn-outline-secondary"
type="button"
disabled={isSending}
onClick={submitForm}
aria-label="Add webhook"
>
{isSending ? (
<>
<span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
<span className="ms-2">{isUndefined(props.webhook) ? 'Adding' : 'Updating'} webhook</span>
</>
) : (
<div className="d-flex flex-row align-items-center text-uppercase">
{isUndefined(props.webhook) ? (
<>
<MdAddCircle className="me-2" />
<span>Add</span>
</>
) : (
<>
<FaPencilAlt className="me-2" />
<div>Save</div>
</>
)}
</div>
)}
</button>
</div>
</div>
<Alert message={apiError} type="danger" onClose={() => setApiError(null)} />
</div>
</form>
</div>
</div>
);
}