react-icons/io#IoIosArrowBack TypeScript Examples

The following examples show how to use react-icons/io#IoIosArrowBack. 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: index.tsx    From dxvote with GNU Affero General Public License v3.0 5 votes vote down vote up
BackButton = styled(IoIosArrowBack)`
  cursor: pointer;
`
Example #2
Source File: index.tsx    From screenshot.rocks with MIT License 5 votes vote down vote up
BrowserFrame = view((props: ICanvasProps) => {
    let browserContent;
    if (!props.showControlsOnly) {
        browserContent = props.imageData
            ? <img id="screenshot"
                   src={props.imageData}
                   alt="Screenshot.rocks browser mockup"/>
            : <ImageSelector/>
    }

    return (
        <div className={styles(props)}>
            <div className="browser-controls">
                <div className={`window-controls ${!browserStore.settings.showWindowControls ? 'hide' : ''}`}>
                    <span className="close"/>
                    <span className="minimise"/>
                    <span className="maximise"/>
                </div>
                <div className={`page-controls ${!browserStore.settings.showNavigationButtons ? 'hide' : ''}`}>
                    <span className="back browser-container">
                        <IoIosArrowBack/>
                    </span>
                    <span className="forward browser-container">
                        <IoIosArrowForward/>
                    </span>
                </div>
                <span className={`url-bar browser-container ${!browserStore.settings.showAddressBar || props.hideAddressBarOverride ? 'hide' : ''}`}>
                    <span className="lock">
                        <FiLock/>
                    </span>
                    <div className={`url-text ${!browserStore.settings.showAddressBarUrl ? 'hide' : ''}`}>
                        <span className="text-success" contentEditable suppressContentEditableWarning>
                            {browserStore.settings.addressBarUrlProtocol}
                        </span>
                        <input
                            className="urlInput"
                            value={browserStore.settings.addressBarUrl}
                            type="text"
                            onInput={(e:FormEvent<HTMLInputElement>) => {browserStore.settings.addressBarUrl = e.currentTarget.value}}>
                        </input>
                    </div>
                    </span>
                <span className={`browser-container ${!browserStore.settings.showSettingsButton ? 'hide' : ''}`}>
                    <span className="settings">
                        <IoIosOptions/>
                    </span>
                </span>
            </div>
            <div className="content-wrap">
                {browserContent}
            </div>
        </div>
    );
})
Example #3
Source File: Form.tsx    From hub with Apache License 2.0 4 votes vote down vote up
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>
  );
}
Example #4
Source File: LogIn.tsx    From hub with Apache License 2.0 4 votes vote down vote up
LogIn = (props: Props) => {
  const { dispatch } = useContext(AppCtx);
  const history = useHistory();
  const loginForm = useRef<HTMLFormElement>(null);
  const emailInput = useRef<RefInputField>(null);
  const passwordInput = useRef<RefInputField>(null);
  const [isLoading, setIsLoading] = useState<Loading>({ status: false });
  const [isValidated, setIsValidated] = useState(false);
  const [visible2FACode, setVisible2FACode] = useState<boolean>(false);
  const [apiError, setApiError] = useState<string | null>(null);
  const [email, setEmail] = useState('');
  const [visibleResetPassword, setVisibleResetPassword] = useState(false);
  const [passcode, setPasscode] = useState<string>('');
  const [isApprovingSession, setIsApprovingSession] = useState<boolean>(false);

  const onPasscodeChange = (e: ChangeEvent<HTMLInputElement>) => {
    setPasscode(e.target.value);
  };

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

  const onEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
  };

  const onCloseModal = () => {
    if (!isUndefined(props.redirect) || !isUndefined(props.visibleModal)) {
      // If redirect option is defined and user closes login modal,
      // querystring with login parameters is cleaned to avoid open modal again on refresh
      history.replace({
        pathname: window.location.pathname,
        search: cleanLoginUrlParams(window.location.search),
      });
    }
    setVisibleResetPassword(false);
    props.setOpenLogIn(false);
  };

  const onLoginError = (err: any) => {
    let error = compoundErrorMessage(err, 'An error occurred signing in');
    if (err.kind === ErrorKind.Unauthorized) {
      error = 'Authentication failed. Please check your credentials.';
    }
    setApiError(error);
    setIsLoading({ status: false });
    setIsApprovingSession(false);
    dispatch(signOut());
  };

  const onLoginSuccess = () => {
    setIsApprovingSession(false);
    setIsLoading({ status: false });
    dispatch(refreshUserProfile(dispatch, props.redirect));
    props.setOpenLogIn(false);
  };

  async function loginUser(user: UserLogin) {
    try {
      await API.login(user);
      onLoginSuccess();
    } catch (err: any) {
      if (err.kind === ErrorKind.NotApprovedSession) {
        setVisible2FACode(true);
      } else {
        onLoginError(err);
      }
    }
  }

  async function approveSession() {
    try {
      setIsApprovingSession(true);
      await API.approveSession(passcode);
      onLoginSuccess();
    } catch (err: any) {
      onLoginError(err);
    }
  }

  const submitForm = () => {
    cleanApiError();
    setIsLoading({ type: 'log', status: true });
    if (loginForm.current) {
      validateForm(loginForm.current).then((validation: FormValidation) => {
        if (validation.isValid && !isNull(validation.user)) {
          loginUser(validation.user);
        } else {
          setIsLoading({ status: false });
        }
      });
    }
  };

  const validateForm = (form: HTMLFormElement): Promise<FormValidation> => {
    let user: UserLogin | null = null;

    return validateAllFields().then((isValid: boolean) => {
      if (isValid) {
        const formData = new FormData(form);
        user = {
          email: formData.get('email') as string,
          password: formData.get('password') as string,
        };
      }
      setIsValidated(true);
      return { isValid, user };
    });
  };

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

  const handleOnReturnKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
    if (event.key === 'Enter' && !isNull(loginForm)) {
      submitForm();
    }
  };

  return (
    <Modal
      header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Sign in</div>}
      modalClassName={styles.modal}
      open={props.openLogIn}
      onClose={onCloseModal}
      error={apiError}
      cleanError={cleanApiError}
    >
      {visibleResetPassword ? (
        <div className="h-100 d-flex flex-column">
          <div>
            <button
              className="btn btn-sm btn-link ps-0 mb-2 text-no-decoration"
              type="button"
              onClick={() => setVisibleResetPassword(false)}
              aria-label="Back to Sign in"
            >
              <div className="d-flex flex-row align-items-center">
                <IoIosArrowBack className="me-2" />
                Back to Sign in
              </div>
            </button>
          </div>

          <div className="flex-grow-1 w-100 d-flex align-items-center">
            <ResetPassword visibleTitle />
          </div>
        </div>
      ) : (
        <>
          {visible2FACode ? (
            <div className="h-100 d-flex flex-column">
              <div>
                <button
                  className="btn btn-sm btn-link ps-0 mb-2 text-no-decoration"
                  type="button"
                  onClick={() => setVisible2FACode(false)}
                  aria-label="Back to Sign in"
                >
                  <div className="d-flex flex-row align-items-center">
                    <IoIosArrowBack className="me-2" />
                    Back to Sign in
                  </div>
                </button>
              </div>

              <div className="flex-grow-1 w-100 d-flex align-items-center">
                <div className="w-100">
                  <InputField
                    type="text"
                    label="Authentication code"
                    name="passcode"
                    autoComplete="off"
                    value={passcode}
                    onChange={onPasscodeChange}
                    invalidText={{
                      default: 'This field is required',
                    }}
                    validateOnBlur
                    required
                  />

                  <button
                    onClick={approveSession}
                    className="btn btn-success btn-sm text-uppercase mt-3"
                    disabled={passcode === '' || isApprovingSession}
                    aria-label="Verify passcode"
                  >
                    {isApprovingSession ? (
                      <>
                        <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                        <span className="ms-2">Verifying...</span>
                      </>
                    ) : (
                      <>Verify</>
                    )}
                  </button>
                </div>
              </div>
            </div>
          ) : (
            <div className="my-auto">
              <form
                ref={loginForm}
                data-testid="loginForm"
                className={classnames('w-100', { 'needs-validation': !isValidated }, { 'was-validated': isValidated })}
                onFocus={cleanApiError}
                autoComplete="on"
                noValidate
              >
                <InputField
                  ref={emailInput}
                  type="email"
                  label="Email"
                  name="email"
                  value=""
                  invalidText={{
                    default: 'This field is required',
                    typeMismatch: 'Please enter a valid email address',
                  }}
                  autoComplete="email"
                  onChange={onEmailChange}
                  validateOnBlur={email !== ''}
                  required
                />

                <InputField
                  ref={passwordInput}
                  type="password"
                  label="Password"
                  name="password"
                  value=""
                  invalidText={{
                    default: 'This field is required',
                  }}
                  validateOnBlur
                  onKeyDown={handleOnReturnKeyDown}
                  autoComplete="current-password"
                  required
                />

                <div className="d-flex flex-row align-items-row justify-content-between">
                  <button
                    className="btn btn-sm btn-link ps-0 text-no-decoration"
                    type="button"
                    onClick={() => setVisibleResetPassword(true)}
                    aria-label="Open reset password"
                  >
                    Forgot password?
                  </button>

                  <button
                    className="btn btn-sm btn-outline-secondary"
                    type="button"
                    disabled={isLoading.status}
                    onClick={submitForm}
                    aria-label="Sign in"
                  >
                    <div className="d-flex flex-row align-items-center text-uppercase">
                      {!isUndefined(isLoading.type) && isLoading.type === 'log' ? (
                        <>
                          <span className="spinner-grow spinner-grow-sm" role="status" aria-hidden="true" />
                          <span className="ms-2">Singing in...</span>
                        </>
                      ) : (
                        <>
                          <FaSignInAlt className="me-2" />
                          <>Sign in</>
                        </>
                      )}
                    </div>
                  </button>
                </div>
              </form>
            </div>
          )}

          {!visible2FACode && <OAuth isLoading={isLoading} setIsLoading={setIsLoading} />}
        </>
      )}
    </Modal>
  );
}
Example #5
Source File: index.tsx    From hub with Apache License 2.0 4 votes vote down vote up
PackageView = (props: Props) => {
  const history = useHistory();
  const point = useBreakpointDetect();
  const contentWrapper = useRef<HTMLDivElement | null>(null);
  const [isLoadingPackage, setIsLoadingPackage] = useState(false);
  const [packageName, setPackageName] = useState(props.packageName);
  const [repositoryKind, setRepositoryKind] = useState(props.repositoryKind);
  const [repositoryName, setRepositoryName] = useState(props.repositoryName);
  const [version, setVersion] = useState(props.version);
  const [detail, setDetail] = useState<Package | null | undefined>(undefined);
  const { tsQueryWeb, tsQuery, pageNumber, filters, deprecated, operators, verifiedPublisher, official, sort } =
    props.searchUrlReferer || {};
  const [apiError, setApiError] = useState<null | string | JSX.Element>(null);
  const [currentHash, setCurrentHash] = useState<string | undefined>(props.hash);
  const columnWrapper = useRef<HTMLDivElement | null>(null);
  const [relatedPosition, setRelatedPosition] = useState<'column' | 'content' | undefined | null>(null);
  const [currentPkgId, setCurrentPkgId] = useState<null | string>(null);
  const [relatedPackages, setRelatedPackages] = useState<Package[] | undefined>(undefined);
  const [viewsStats, setViewsStats] = useState<PackageViewsStats | undefined>();

  useScrollRestorationFix();

  useLayoutEffect(() => {
    const updateRelatedPosition = () => {
      if (contentWrapper.current && columnWrapper.current && point && detail) {
        if (point && !['xs', 'sm'].includes(point)) {
          setRelatedPosition(
            contentWrapper.current.offsetHeight <= columnWrapper.current.offsetHeight + RELATED_PKGS_GAP
              ? 'content'
              : 'column'
          );
        } else {
          setRelatedPosition('column');
        }
      }
    };

    if (isUndefined(relatedPosition)) {
      updateRelatedPosition();
    }
  }, [relatedPosition]); /* eslint-disable-line react-hooks/exhaustive-deps */

  useEffect(() => {
    if (!isUndefined(props.packageName) && !isLoadingPackage) {
      setPackageName(props.packageName);
      setVersion(props.version);
      setRepositoryKind(props.repositoryKind);
      setRepositoryName(props.repositoryName);
    }
  }, [props, isLoadingPackage]);

  useEffect(() => {
    async function fetchRelatedPackages(pkgDetail: Package) {
      try {
        let name = pkgDetail.name.split('-');
        let words = [...name];
        if (!isUndefined(pkgDetail.keywords) && pkgDetail.keywords.length > 0) {
          words = [...name, ...pkgDetail.keywords];
        }
        const searchResults = await API.searchPackages(
          {
            tsQueryWeb: Array.from(new Set(words)).join(' or '),
            filters: {},
            limit: 9,
            offset: 0,
          },
          false
        );
        let filteredPackages: Package[] = [];
        if (!isNull(searchResults.packages)) {
          filteredPackages = searchResults.packages
            .filter((item: Package) => item.packageId !== currentPkgId)
            .slice(0, 8); // Only first 8 packages
        }
        setRelatedPackages(filteredPackages);
      } catch {
        setRelatedPackages([]);
      }
    }
    if (!isNull(currentPkgId) && detail) {
      fetchRelatedPackages(detail);
    }
  }, [currentPkgId]); /* eslint-disable-line react-hooks/exhaustive-deps */

  async function trackView(pkgID: string, version: string) {
    try {
      API.trackView(pkgID, version);
    } catch {
      // Do not do anything
    }
  }

  async function getViewsStats(pkgID: string) {
    try {
      setViewsStats(await API.getViews(pkgID));
    } catch (err: any) {
      // Don't display any error if API request fails
    }
  }

  async function fetchPackageDetail() {
    try {
      setRelatedPosition(null);
      const detailPkg = await API.getPackage({
        packageName: packageName,
        version: version,
        repositoryKind: repositoryKind,
        repositoryName: repositoryName,
      });
      let metaTitle = `${detailPkg.normalizedName} ${detailPkg.version} ยท ${
        detailPkg.repository.userAlias || detailPkg.repository.organizationName
      }/${detailPkg.repository.name}`;
      updateMetaIndex(metaTitle, detailPkg.description);
      setDetail(detailPkg);
      // Track view
      trackView(detailPkg.packageId, detailPkg.version!);
      // Get pkg views stats
      getViewsStats(detailPkg.packageId);
      if (currentHash) {
        setCurrentHash(undefined);
      }
      setApiError(null);
      setCurrentPkgId(detailPkg.packageId);
      setRelatedPosition(undefined);
      window.scrollTo(0, 0); // Scroll to top when a new version is loaded
      setIsLoadingPackage(false);
      scrollIntoView();
    } catch (err: any) {
      if (err.kind === ErrorKind.NotFound) {
        setApiError(
          <>
            <div className={`mb-4 mb-lg-5 h2 ${styles.noDataTitleContent}`}>
              Sorry, the package you requested was not found.
            </div>

            <p className={`h5 mb-4 mb-lg-5 ${styles.noDataTitleContent}`}>
              The package you are looking for may have been deleted by the provider, or it may now belong to a different
              repository. Please try searching for it, as it may help locating the package in a different repository or
              discovering other alternatives.
            </p>

            <p className="h6 lh-base">
              NOTE: The official Helm <span className="fw-bold">stable</span> and{' '}
              <span className="fw-bold">incubator</span> repositories were removed from Artifact Hub on November 6th as
              part of the deprecation plan announced by the Helm project. For more information please see{' '}
              <ExternalLink href="https://helm.sh/blog/charts-repo-deprecation/" label="Open Helm documentation">
                this blog post
              </ExternalLink>{' '}
              and{' '}
              <ExternalLink href="https://github.com/helm/charts/issues/23944" label="Open GitHub issue">
                this GitHub issue
              </ExternalLink>
              .
            </p>
          </>
        );
      } else if (!isUndefined(err.message)) {
        setApiError(err.message);
      }
      setDetail(null);
      setIsLoadingPackage(false);
    }
  }

  useEffect(() => {
    setIsLoadingPackage(true);
    fetchPackageDetail();
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [packageName, version, repositoryName, repositoryKind, setIsLoadingPackage]);
  /* eslint-enable react-hooks/exhaustive-deps */

  useEffect(() => {
    return () => {
      setIsLoadingPackage(false);
    };
  }, [setIsLoadingPackage]);

  let sortedVersions: Version[] = [];
  if (detail && detail.availableVersions) {
    sortedVersions =
      detail.repository.kind === RepositoryKind.Container
        ? detail.availableVersions
        : sortPackageVersions(detail.availableVersions);
  }

  // Section for recommended packages and in production (orgs)
  const renderMoreDetails = (): JSX.Element | null => {
    if (detail) {
      const recommendations = detail.recommendations && detail.recommendations.length > 0;

      if (recommendations) {
        return (
          <div
            data-testid="more-details-section"
            className={`d-none d-md-block px-3 ${styles.moreDetailsSectionWrapper}`}
          >
            <div className="container-lg px-sm-4 px-lg-0 py-2 d-flex flex-column position-relative">
              {recommendations && <RecommendedPackages recommendations={detail.recommendations} className="mt-3" />}
            </div>
          </div>
        );
      } else {
        return null;
      }
    }
    return null;
  };

  const getInstallationModal = (wrapperClassName?: string): JSX.Element | null => (
    <div className={wrapperClassName}>
      <InstallationModal
        package={detail}
        visibleInstallationModal={!isUndefined(props.visibleModal) && props.visibleModal === 'install'}
        searchUrlReferer={props.searchUrlReferer}
        fromStarredPage={props.fromStarredPage}
      />
    </div>
  );

  const getFalcoRules = (): ContentDefaultModalItem[] | undefined => {
    let rules: ContentDefaultModalItem[] | undefined;
    if (
      !isUndefined(detail) &&
      !isNull(detail) &&
      !isNull(detail.data) &&
      !isUndefined(detail.data) &&
      !isUndefined(detail.data.rules)
    ) {
      if (isArray(detail.data.rules)) {
        rules = detail.data.rules.map((item: any, index: number) => {
          return {
            name:
              item.Name && item.Name !== ''
                ? item.Name
                : `rules${detail!.data!.rules!.length === 1 ? '' : `-${index + 1}`}`,
            file: item.Raw,
          };
        });
      } else {
        rules = Object.keys(detail.data.rules).map((rulesFileName: string) => {
          return {
            name: rulesFileName,
            file: (detail!.data!.rules as FalcoRules)[rulesFileName],
          };
        });
      }
    }
    return rules;
  };

  const getOPAPolicies = (): ContentDefaultModalItem[] | undefined => {
    let policies: ContentDefaultModalItem[] | undefined;
    if (
      !isUndefined(detail) &&
      !isNull(detail) &&
      !isNull(detail.data) &&
      !isUndefined(detail.data) &&
      !isUndefined(detail.data.policies)
    ) {
      policies = Object.keys(detail.data.policies).map((policyName: string) => {
        return {
          name: policyName,
          file: detail.data!.policies![policyName],
        };
      });
    }
    return policies;
  };

  const getManifestRaw = (): string | undefined => {
    let manifest: string | undefined;
    if (
      !isUndefined(detail) &&
      !isNull(detail) &&
      !isNull(detail.data) &&
      !isUndefined(detail.data) &&
      !isUndefined(detail.data.manifestRaw)
    ) {
      manifest = detail.data.manifestRaw as string;
    }
    return manifest;
  };

  const getCRDs = (): CustomResourcesDefinition[] | undefined => {
    let resources: CustomResourcesDefinition[] | undefined;
    if (detail && detail.crds) {
      let examples: CustomResourcesDefinitionExample[] = detail.crdsExamples || [];
      resources = detail.crds.map((resourceDefinition: CustomResourcesDefinition) => {
        return {
          ...resourceDefinition,
          example: examples.find((info: any) => info.kind === resourceDefinition.kind),
        };
      });
    }
    return resources;
  };

  const getBadges = (withRepoInfo: boolean, extraStyle?: string): JSX.Element => (
    <>
      <OfficialBadge official={isPackageOfficial(detail)} className={`d-inline me-3 ${extraStyle}`} type="package" />
      {withRepoInfo && (
        <VerifiedPublisherBadge
          verifiedPublisher={detail!.repository.verifiedPublisher}
          className={`d-inline me-3 ${extraStyle}`}
        />
      )}
      {detail!.deprecated && (
        <Label
          text="Deprecated"
          icon={<AiOutlineStop />}
          labelStyle="danger"
          className={`d-inline me-3 ${extraStyle}`}
        />
      )}
      <SignedBadge
        repositoryKind={detail!.repository.kind}
        signed={detail!.signed}
        signatures={detail!.signatures}
        className={`d-inline ${extraStyle}`}
      />
      <div className="d-none d-lg-inline">
        <SignKeyInfo
          visibleKeyInfo={!isUndefined(props.visibleModal) && props.visibleModal === 'key-info'}
          repoKind={detail!.repository.kind}
          signatures={detail!.signatures}
          signed={detail!.signed}
          signKey={detail!.signKey}
          searchUrlReferer={props.searchUrlReferer}
          fromStarredPage={props.fromStarredPage}
        />
      </div>
    </>
  );

  useEffect(() => {
    if (props.hash !== currentHash) {
      setCurrentHash(props.hash);
      if (isUndefined(props.hash) || props.hash === '') {
        window.scrollTo(0, 0);
      } else {
        scrollIntoView();
      }
    }
  }, [props.hash]); /* eslint-disable-line react-hooks/exhaustive-deps */

  const scrollIntoView = useCallback(
    (id?: string) => {
      const elId = id || props.hash;
      if (isUndefined(elId) || elId === '') return;

      try {
        const element = document.querySelector(elId);
        if (element) {
          element.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });

          if (isUndefined(id)) {
            history.replace({
              pathname: history.location.pathname,
              hash: elId,
              state: {
                searchUrlReferer: props.searchUrlReferer,
                fromStarredPage: props.fromStarredPage,
              },
            });
          } else if (props.hash !== elId) {
            history.push({
              pathname: history.location.pathname,
              hash: elId,
              state: {
                searchUrlReferer: props.searchUrlReferer,
                fromStarredPage: props.fromStarredPage,
              },
            });
          }
        }
      } finally {
        return;
      }
    },
    [props.hash, props.searchUrlReferer, props.fromStarredPage, history]
  );

  const getSupportLink = (): string | undefined => {
    if (detail && detail.links) {
      const support = detail.links.find((link: PackageLink) => link.name.toLowerCase() === 'support');
      if (support) {
        return support.url;
      } else {
        return undefined;
      }
    }

    return undefined;
  };

  const getAdditionalPkgContent = (): { content: JSX.Element; titles: string } | null => {
    if (isNull(detail) || isUndefined(detail)) return null;
    let additionalTitles = '';
    const additionalContent = (
      <>
        {(() => {
          switch (detail.repository.kind) {
            case RepositoryKind.Krew:
              let manifest: string | undefined = getManifestRaw();
              if (!isUndefined(manifest)) {
                additionalTitles += '# Manifest\n';
              }
              return (
                <>
                  {!isUndefined(manifest) && (
                    <div className={`mb-5 ${styles.codeWrapper}`}>
                      <AnchorHeader level={2} scrollIntoView={scrollIntoView} title="Manifest" />

                      <div
                        className={`d-flex d-xxxl-inline-block mw-100 position-relative overflow-hidden border ${styles.manifestWrapper}`}
                      >
                        <BlockCodeButtons content={manifest} filename={`${detail.normalizedName}-rules.yaml`} />
                        <SyntaxHighlighter
                          language="yaml"
                          style={docco}
                          customStyle={{
                            backgroundColor: 'transparent',
                            padding: '1.5rem',
                            lineHeight: '1.25rem',
                            marginBottom: '0',
                            height: '100%',
                            fontSize: '80%',
                            color: '#636a6e',
                          }}
                          lineNumberStyle={{
                            color: 'var(--color-black-25)',
                            marginRight: '5px',
                            fontSize: '0.8rem',
                          }}
                          showLineNumbers
                        >
                          {manifest}
                        </SyntaxHighlighter>
                      </div>
                    </div>
                  )}
                </>
              );

            default:
              return null;
          }
        })()}
      </>
    );

    return { content: additionalContent, titles: additionalTitles };
  };

  const supportLink: string | undefined = getSupportLink();

  const additionalInfo = getAdditionalPkgContent();

  return (
    <>
      {!isUndefined(props.searchUrlReferer) && (
        <SubNavbar>
          <button
            className={`btn btn-link btn-sm ps-0 d-flex align-items-center ${styles.link}`}
            onClick={() => {
              history.push({
                pathname: '/packages/search',
                search: prepareQueryString({
                  pageNumber: pageNumber || 1,
                  tsQueryWeb: tsQueryWeb,
                  tsQuery: tsQuery,
                  filters: filters,
                  deprecated: deprecated,
                  operators: operators,
                  verifiedPublisher: verifiedPublisher,
                  official: official,
                  sort: sort,
                }),
                state: { 'from-detail': true },
              });
            }}
            aria-label="Back to results"
          >
            <IoIosArrowBack className="me-2" />
            {tsQueryWeb ? (
              <>
                Back to "<span className="fw-bold">{tsQueryWeb}</span>" results
              </>
            ) : (
              <>
                Back to
                <span className={`fw-bold ${styles.extraSpace}`}> search results</span>
              </>
            )}
          </button>
        </SubNavbar>
      )}

      {!isUndefined(props.fromStarredPage) && props.fromStarredPage && (
        <SubNavbar>
          <button
            className={`btn btn-link btn-sm ps-0 d-flex align-items-center ${styles.link}`}
            onClick={() => {
              history.push({
                pathname: '/packages/starred',
                state: { 'from-detail': true },
              });
            }}
            aria-label="Back to starred packages"
          >
            <IoIosArrowBack className="me-2" />
            <div>
              Back to <span className="fw-bold">starred packages</span>
            </div>
          </button>
        </SubNavbar>
      )}

      <div data-testid="mainPackage" className="position-relative flex-grow-1">
        {(isLoadingPackage || isUndefined(detail)) && <Loading spinnerClassName="position-fixed top-50" />}

        {!isUndefined(detail) && (
          <>
            {!isNull(detail) && (
              <>
                <div className={`jumbotron package-detail-jumbotron rounded-0 mb-2 ${styles.jumbotron}`}>
                  <div className="container-lg px-sm-4 px-lg-0 position-relative">
                    <div className="d-flex align-items-start w-100 mb-3">
                      <div className="d-flex align-items-center flex-grow-1 mw-100">
                        <div
                          className={`d-flex align-items-center justify-content-center p-1 p-md-2 overflow-hidden border border-2 rounded-circle bg-white ${styles.imageWrapper} imageWrapper`}
                        >
                          <Image
                            className={styles.image}
                            alt={detail.displayName || detail.name}
                            imageId={detail.logoImageId}
                            kind={detail.repository.kind}
                          />
                        </div>

                        <div className={`ms-3 flex-grow-1 ${styles.wrapperWithContentEllipsis}`}>
                          <div className={`d-flex flex-row align-items-center ${styles.titleWrapper}`}>
                            <div className={`h3 mb-0 text-nowrap text-truncate ${styles.title}`}>
                              {detail.displayName || detail.name}
                            </div>
                            <div className="d-none d-md-flex ms-3">{getBadges(false, 'mt-1')}</div>
                          </div>

                          <div className={`d-flex d-md-none text-truncate mt-2 w-100 ${styles.mobileSubtitle}`}>
                            <small className="text-muted text-uppercase">Repo: </small>
                            <div className={`mx-1 d-inline ${styles.mobileIcon}`}>
                              <RepositoryIcon kind={detail.repository.kind} className={`w-auto ${styles.repoIcon}`} />
                            </div>
                            <span className={`text-dark d-inline-block text-truncate mw-100 ${styles.mobileVersion}`}>
                              {detail.repository.displayName || detail.repository.name}
                            </span>
                          </div>

                          <div className={`d-none d-md-flex flex-row align-items-baseline mt-2 ${styles.subtitle}`}>
                            {detail.repository.userAlias ? (
                              <div className={`me-2 text-truncate ${styles.mw50}`}>
                                <small className="me-1 text-uppercase text-muted">User: </small>

                                <Link
                                  className="text-dark"
                                  to={{
                                    pathname: '/packages/search',
                                    search: prepareQueryString({
                                      pageNumber: 1,
                                      filters: {
                                        user: [detail.repository.userAlias!],
                                      },
                                      deprecated: detail.deprecated || false,
                                    }),
                                  }}
                                >
                                  {detail.repository.userAlias}
                                </Link>
                              </div>
                            ) : (
                              <OrganizationInfo
                                className={`me-2 text-truncate d-flex flex-row align-items-baseline ${styles.mw50}`}
                                organizationName={detail.repository.organizationName!}
                                organizationDisplayName={detail.repository.organizationDisplayName}
                                deprecated={detail.deprecated}
                                visibleLegend
                              />
                            )}

                            <RepositoryInfo
                              repository={detail.repository}
                              deprecated={detail.deprecated}
                              className={`text-truncate d-flex flex-row align-items-baseline ${styles.mw50}`}
                              repoLabelClassName={styles.repoLabel}
                              visibleIcon
                              withLabels
                            />
                          </div>
                        </div>
                      </div>
                    </div>

                    <p className={`mb-0 overflow-hidden text-break ${styles.description}`}>{detail.description}</p>

                    <Stats
                      packageStats={detail.stats}
                      productionOrganizationsCount={detail.productionOrganizationsCount}
                    />

                    <div className="d-flex flex-wrap d-md-none">{getBadges(true, 'mt-3 mt-md-0')}</div>

                    <div
                      className={`position-absolute d-flex flex-row align-items-center top-0 end-0 ${styles.optsWrapper}`}
                    >
                      {detail!.ts && !isFuture(detail!.ts) && (
                        <span className={`d-block d-md-none text-muted text-nowrap ${styles.date}`}>
                          Updated {moment.unix(detail!.ts).fromNow()}
                        </span>
                      )}
                      <StarButton packageId={detail.packageId} />
                      <SubscriptionsButton packageId={detail.packageId} />
                      <InProductionButton normalizedName={detail.normalizedName} repository={detail.repository} />
                      <MoreActionsButton
                        packageId={detail.packageId}
                        packageName={detail.displayName || detail.name}
                        packageDescription={detail.description}
                        visibleWidget={!isUndefined(props.visibleModal) && props.visibleModal === 'widget'}
                        searchUrlReferer={props.searchUrlReferer}
                        fromStarredPage={props.fromStarredPage}
                      />
                    </div>

                    <div className="row align-items-baseline d-md-none">
                      <Modal
                        buttonType="btn-outline-secondary btn-sm text-nowrap"
                        buttonContent={
                          <>
                            <FiPlus className="me-2" />
                            <span>Info</span>
                          </>
                        }
                        header={
                          <ModalHeader
                            displayName={detail.displayName}
                            name={detail.name}
                            logoImageId={detail.logoImageId}
                            repoKind={detail.repository.kind}
                          />
                        }
                        className={`col mt-3 ${styles.btnMobileWrapper}`}
                      >
                        <Details
                          package={detail}
                          sortedVersions={sortedVersions}
                          channels={detail.channels}
                          viewsStats={viewsStats}
                          version={props.version}
                          searchUrlReferer={props.searchUrlReferer}
                          fromStarredPage={props.fromStarredPage}
                          visibleSecurityReport={false}
                        />
                      </Modal>

                      {getInstallationModal(`col mt-3 ${styles.btnMobileWrapper}`)}

                      {point && ['xs', 'sm'].includes(point) && (
                        <div className={`col mt-3 ${styles.btnMobileWrapper}`}>
                          <ChangelogModal
                            packageId={detail.packageId}
                            normalizedName={detail.normalizedName}
                            repository={detail.repository}
                            hasChangelog={detail.hasChangelog!}
                            currentVersion={props.version}
                            visibleChangelog={!isUndefined(props.visibleModal) && props.visibleModal === 'changelog'}
                            visibleVersion={
                              !isUndefined(props.visibleModal) && props.visibleModal === 'changelog'
                                ? props.visibleVersion
                                : undefined
                            }
                            searchUrlReferer={props.searchUrlReferer}
                            fromStarredPage={props.fromStarredPage}
                          />
                        </div>
                      )}
                    </div>
                  </div>
                </div>

                {renderMoreDetails()}
              </>
            )}

            <div className="container-lg px-sm-4 px-lg-0">
              {isNull(detail) && !isLoadingPackage ? (
                <NoData className={styles.noDataWrapper}>
                  {isNull(apiError) ? (
                    <>An error occurred getting this package, please try again later.</>
                  ) : (
                    <>{apiError}</>
                  )}
                </NoData>
              ) : (
                <div className="d-flex flex-column-reverse d-md-block px-xs-0 px-sm-3 px-lg-0">
                  <div
                    className={`ms-0 ms-md-5 mb-5 position-relative float-none float-md-end ${styles.additionalInfo}`}
                  >
                    {!isNull(detail) && (
                      <div ref={columnWrapper} className={styles.rightColumnWrapper}>
                        <div className="d-none d-md-block">
                          {getInstallationModal('mb-2')}

                          <div className="d-none d-lg-block">
                            <ChartTemplatesModal
                              normalizedName={detail.normalizedName}
                              packageId={detail.packageId}
                              version={detail.version!}
                              sortedVersions={sortedVersions}
                              repoKind={detail.repository.kind}
                              visibleChartTemplates={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'template'
                              }
                              visibleTemplate={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'template'
                                  ? props.visibleTemplate
                                  : undefined
                              }
                              compareVersionTo={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'template'
                                  ? props.compareVersionTo
                                  : undefined
                              }
                              searchUrlReferer={props.searchUrlReferer}
                              fromStarredPage={props.fromStarredPage}
                            />
                          </div>

                          <div className="d-none d-lg-block">
                            <ContentDefaultModal
                              kind={ContentDefaultModalKind.CustomResourcesDefinition}
                              packageId={detail.packageId}
                              modalName="crds"
                              language="yaml"
                              visibleModal={!isUndefined(props.visibleModal) && props.visibleModal === 'crds'}
                              visibleFile={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'crds'
                                  ? props.visibleFile
                                  : undefined
                              }
                              btnModalContent={
                                <div className="d-flex flex-row align-items-center justify-content-center">
                                  <FiCode />
                                  <span className="ms-2 fw-bold text-uppercase">CRDs</span>
                                </div>
                              }
                              normalizedName={detail.normalizedName}
                              title="Custom Resources Definition"
                              files={getCRDs() as any}
                              searchUrlReferer={props.searchUrlReferer}
                              fromStarredPage={props.fromStarredPage}
                            />
                          </div>

                          <div className="d-none d-lg-block">
                            <ContentDefaultModal
                              kind={ContentDefaultModalKind.Rules}
                              packageId={detail.packageId}
                              modalName="rules"
                              language="yaml"
                              visibleModal={!isUndefined(props.visibleModal) && props.visibleModal === 'rules'}
                              visibleFile={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'rules'
                                  ? props.visibleFile
                                  : undefined
                              }
                              btnModalContent={
                                <div className="d-flex flex-row align-items-center justify-content-center">
                                  <FiCode />
                                  <span className="ms-2 fw-bold text-uppercase">Rules</span>
                                </div>
                              }
                              normalizedName={detail.normalizedName}
                              title="Rules"
                              files={getFalcoRules() as any}
                              searchUrlReferer={props.searchUrlReferer}
                              fromStarredPage={props.fromStarredPage}
                            />
                          </div>

                          <div className="d-none d-lg-block">
                            <ContentDefaultModal
                              kind={ContentDefaultModalKind.Policy}
                              packageId={detail.packageId}
                              modalName="policies"
                              language="text"
                              visibleModal={!isUndefined(props.visibleModal) && props.visibleModal === 'policies'}
                              visibleFile={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'policies'
                                  ? props.visibleFile
                                  : undefined
                              }
                              btnModalContent={
                                <div className="d-flex flex-row align-items-center justify-content-center">
                                  <FiCode />
                                  <span className="ms-2 fw-bold text-uppercase">Policies</span>
                                </div>
                              }
                              normalizedName={detail.normalizedName}
                              title="Policies"
                              files={getOPAPolicies() as any}
                              searchUrlReferer={props.searchUrlReferer}
                              fromStarredPage={props.fromStarredPage}
                            />
                          </div>

                          {(() => {
                            switch (detail.repository.kind) {
                              case RepositoryKind.TektonTask:
                              case RepositoryKind.TektonPipeline:
                                return (
                                  <TektonManifestModal
                                    normalizedName={detail.normalizedName}
                                    manifestRaw={getManifestRaw()}
                                    searchUrlReferer={props.searchUrlReferer}
                                    fromStarredPage={props.fromStarredPage}
                                    visibleManifest={
                                      !isUndefined(props.visibleModal) && props.visibleModal === 'manifest'
                                    }
                                  />
                                );

                              case RepositoryKind.Helm:
                                return (
                                  <>
                                    <div className="mb-2">
                                      <Values
                                        packageId={detail.packageId}
                                        version={detail.version!}
                                        normalizedName={detail.normalizedName}
                                        sortedVersions={sortedVersions}
                                        searchUrlReferer={props.searchUrlReferer}
                                        fromStarredPage={props.fromStarredPage}
                                        visibleValues={
                                          !isUndefined(props.visibleModal) && props.visibleModal === 'values'
                                        }
                                        visibleValuesPath={
                                          !isUndefined(props.visibleModal) && props.visibleModal === 'values'
                                            ? props.visibleValuesPath
                                            : undefined
                                        }
                                        compareVersionTo={
                                          !isUndefined(props.visibleModal) && props.visibleModal === 'values'
                                            ? props.compareVersionTo
                                            : undefined
                                        }
                                      />
                                    </div>
                                    {detail.hasValuesSchema && (
                                      <div className="mb-2">
                                        <ValuesSchema
                                          packageId={detail.packageId}
                                          version={detail.version!}
                                          normalizedName={detail.normalizedName}
                                          searchUrlReferer={props.searchUrlReferer}
                                          fromStarredPage={props.fromStarredPage}
                                          visibleValuesSchema={
                                            !isUndefined(props.visibleModal) && props.visibleModal === 'values-schema'
                                          }
                                          visibleValuesSchemaPath={
                                            !isUndefined(props.visibleModal) && props.visibleModal === 'values-schema'
                                              ? props.visibleValuesPath
                                              : undefined
                                          }
                                        />
                                      </div>
                                    )}
                                  </>
                                );

                              default:
                                return null;
                            }
                          })()}
                          {point && !['xs', 'sm'].includes(point) && (
                            <div className="mb-2">
                              <ChangelogModal
                                packageId={detail.packageId}
                                normalizedName={detail.normalizedName}
                                repository={detail.repository}
                                hasChangelog={detail.hasChangelog!}
                                currentVersion={props.version}
                                visibleChangelog={
                                  !isUndefined(props.visibleModal) && props.visibleModal === 'changelog'
                                }
                                visibleVersion={
                                  !isUndefined(props.visibleModal) && props.visibleModal === 'changelog'
                                    ? props.visibleVersion
                                    : undefined
                                }
                                searchUrlReferer={props.searchUrlReferer}
                                fromStarredPage={props.fromStarredPage}
                              />
                            </div>
                          )}

                          {!isUndefined(detail.screenshots) && (
                            <ScreenshotsModal
                              screenshots={detail.screenshots}
                              visibleScreenshotsModal={
                                !isUndefined(props.visibleModal) && props.visibleModal === 'screenshots'
                              }
                              searchUrlReferer={props.searchUrlReferer}
                              fromStarredPage={props.fromStarredPage}
                            />
                          )}

                          <div className={`card shadow-sm position-relative info ${styles.info}`}>
                            <div className={`card-body ${styles.detailsBody}`}>
                              <Details
                                package={detail}
                                sortedVersions={sortedVersions}
                                channels={detail.channels}
                                searchUrlReferer={props.searchUrlReferer}
                                fromStarredPage={props.fromStarredPage}
                                visibleSecurityReport={
                                  !isUndefined(props.visibleModal) && props.visibleModal === 'security-report'
                                }
                                visibleImage={props.visibleImage}
                                visibleTarget={props.visibleTarget}
                                visibleSection={props.visibleSection}
                                viewsStats={viewsStats}
                                version={props.version}
                                eventId={
                                  !isUndefined(props.visibleModal) && props.visibleModal === 'security-report'
                                    ? props.eventId
                                    : undefined
                                }
                              />
                            </div>
                          </div>
                        </div>

                        {!isUndefined(relatedPosition) && relatedPosition === 'column' && (
                          <div className={styles.relatedPackagesWrapper}>
                            <RelatedPackages packages={relatedPackages} in={relatedPosition} />
                          </div>
                        )}
                      </div>
                    )}
                  </div>

                  {!isNull(detail) && (
                    <>
                      <div
                        className={`noFocus ${styles.mainContent}`}
                        id="content"
                        tabIndex={-1}
                        aria-label="Package detail"
                      >
                        <div ref={contentWrapper}>
                          {isNull(detail.readme) || isUndefined(detail.readme) ? (
                            <div className={styles.contentWrapper}>
                              <NoData className="w-100 noReadmeAlert bg-transparent">
                                <div>
                                  <div className={`mb-4 ${styles.fileIcon}`}>
                                    <IoDocumentTextOutline />
                                  </div>
                                  <p className="h4 mb-3">This package version does not provide a README file</p>
                                </div>
                              </NoData>
                            </div>
                          ) : (
                            <ReadmeWrapper
                              packageName={detail.displayName || detail.name}
                              supportLink={supportLink}
                              markdownContent={detail.readme}
                              scrollIntoView={scrollIntoView}
                              additionalTitles={isNull(additionalInfo) ? '' : additionalInfo.titles}
                            />
                          )}

                          {!isNull(additionalInfo) && <>{additionalInfo.content}</>}
                        </div>

                        <PackagesViewsStats
                          stats={viewsStats}
                          version={props.version}
                          repoKind={detail.repository.kind}
                          title={
                            <AnchorHeader
                              level={2}
                              scrollIntoView={scrollIntoView}
                              anchorName="views"
                              title="Views over the last 30 days"
                            />
                          }
                        />

                        {!isUndefined(relatedPosition) && relatedPosition === 'content' && (
                          <RelatedPackages
                            className={styles.relatedWrapper}
                            packages={relatedPackages}
                            title={<AnchorHeader level={2} scrollIntoView={scrollIntoView} title="Related packages" />}
                            in={relatedPosition}
                          />
                        )}
                      </div>
                    </>
                  )}
                </div>
              )}
            </div>
          </>
        )}
      </div>

      <Footer isHidden={isLoadingPackage || isUndefined(detail)} />
    </>
  );
}