hooks#useToggle TypeScript Examples

The following examples show how to use hooks#useToggle. 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: ReadMore.tsx    From one-platform with MIT License 6 votes vote down vote up
ReadMore = ({
  children,
  component,
  limit = 300,
  showMoreText = '... Read more',
  showLessText = 'Show Less',
  ...props
}: Props & TextProps): JSX.Element => {
  const [isReadMore, setReadMore] = useToggle(true);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const Component: any = component || Fragment;
  const text = children;

  if (typeof text !== 'string' && !Array.isArray(text)) {
    throw Error('String required');
  }

  const isReadMoreHidden = text.length <= limit;

  return (
    <Component {...props}>
      {isReadMore ? text.slice(0, limit) : text}
      {!isReadMoreHidden && (
        <Button
          onClick={setReadMore.toggle}
          variant="link"
          isInline
          className="pf-u-ml-sm pf-u-font-size-sm"
        >
          {isReadMore ? showMoreText : showLessText}
        </Button>
      )}
    </Component>
  );
}
Example #2
Source File: Select.tsx    From one-platform with MIT License 6 votes vote down vote up
Select = ({ children, onSelect, ...selectProps }: Props): JSX.Element => {
  const [isOpen, setIsOpen] = useToggle();

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const onPfeSelect = (...args: any) => {
    // eslint-disable-next-line prefer-spread
    onSelect?.apply(null, args);
    setIsOpen.off();
  };

  return (
    <PfeSelect {...selectProps} onSelect={onPfeSelect} isOpen={isOpen} onToggle={setIsOpen.toggle}>
      {children}
    </PfeSelect>
  );
}
Example #3
Source File: ReadMore.tsx    From one-platform with MIT License 6 votes vote down vote up
ReadMore = ({
  children,
  component,
  limit = 300,
  ...props
}: Props & TextProps): JSX.Element => {
  const [isReadMore, setReadMore] = useToggle(true);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const Component: any = component || Text;
  const text = children;

  if (typeof text !== 'string') {
    throw Error('String required');
  }

  const isReadMoreHidden = text.length <= limit;

  return (
    <Component {...props}>
      {isReadMore ? text.slice(0, limit) : text}
      {!isReadMoreHidden && (
        <>
          {isReadMore && '... '}
          <Button onClick={setReadMore.toggle} variant="link" isInline>
            {isReadMore ? 'Read more' : ' Show less'}
          </Button>
        </>
      )}
    </Component>
  );
}
Example #4
Source File: AsyncSelect.tsx    From one-platform with MIT License 5 votes vote down vote up
AsyncSelect = ({
  render,
  onSelect,
  customFilter,
  onTypeaheadInputChanged,
  ...selectProps
}: Props): JSX.Element => {
  const [isOpen, setIsOpen] = useToggle();
  const [options, setOptions] = useState<ReactElement<any, string | JSXElementConstructor<any>>[]>(
    []
  );

  const [typeAhead, setTypeAhead] = useState('');

  useEffect(() => {
    if (!isOpen) {
      setTypeAhead('');
      setOptions([]);
      return;
    }

    setOptions(LOADING);
    render(typeAhead)
      .then((loadedOptions) => {
        setOptions(loadedOptions);
      })
      .catch(() => {
        setOptions([
          <SelectOption
            key="option-error"
            value="Failed to fetch request"
            isPlaceholder
            isDisabled
          />,
        ]);
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [typeAhead, isOpen]);

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const onPfeSelect = (...args: any) => {
    // eslint-disable-next-line prefer-spread
    onSelect?.apply(null, args);
    setIsOpen.off();
  };

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const onPfeTypeAheadChange = (value: string) => {
    setTypeAhead(value);
    // eslint-disable-next-line prefer-spread
    if (onTypeaheadInputChanged) onTypeaheadInputChanged(value);
  };

  const onFilter = (a: ChangeEvent<HTMLInputElement> | null, value: string) => {
    if (!value) {
      return options;
    }

    if (!customFilter) return options;

    return options.filter((child) => customFilter(child));
  };

  return (
    <PfeSelect
      {...selectProps}
      onSelect={onPfeSelect}
      isOpen={isOpen}
      onToggle={setIsOpen.toggle}
      onTypeaheadInputChanged={onPfeTypeAheadChange}
      onFilter={customFilter && onFilter}
    >
      {options}
    </PfeSelect>
  );
}
Example #5
Source File: AppLayout.tsx    From one-platform with MIT License 5 votes vote down vote up
AppLayout: FC = () => {
  const { pathname } = useLocation();
  const [isSidebarOpen, setIsSidebarOpen] = useToggle(true);
  const { breadcrumbs } = useBreadcrumb();
  const isBreadcrumbHidden = breadcrumbs.length === 0;

  return (
    <Page
      mainContainerId="app-layout-page"
      sidebar={<Sidebar isOpen={isSidebarOpen} />}
      className={styles['app-layout']}
      breadcrumb={
        !isBreadcrumbHidden && (
          <Breadcrumb className={styles['app-layout--breadcrumb']}>
            <BreadcrumbItem>
              <Button variant="link" className="pf-u-p-0" onClick={setIsSidebarOpen.toggle}>
                <BarsIcon />
              </Button>
            </BreadcrumbItem>
            <BreadcrumbItem to="/">One Platform</BreadcrumbItem>
            <BreadcrumbItem>
              {pathname === config.baseURL ? 'API Catalog Home' : <Link to="/">API Catalog</Link>}
            </BreadcrumbItem>
            {breadcrumbs.map(({ label, url }, index) => {
              const isActive = index === breadcrumbs.length - 1;
              return (
                <BreadcrumbItem key={label}>
                  {isActive ? label : <Link to={url}>{label}</Link>}
                </BreadcrumbItem>
              );
            })}
          </Breadcrumb>
        )
      }
    >
      <PageSection
        className={styles['app-layout--content']}
        variant="light"
        padding={{ default: 'noPadding' }}
      >
        <Outlet />
      </PageSection>
    </Page>
  );
}
Example #6
Source File: SwaggerToolboxPage.tsx    From one-platform with MIT License 5 votes vote down vote up
SwaggerToolboxPage = (): JSX.Element => {
  const { envSlug } = useParams();
  const navigate = useNavigate();
  const { pathname } = useLocation();
  const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
  const [isDecodingFile, setIsDecodingFile] = useToggle();

  const schema = schemaData?.fetchAPISchema?.schema;
  const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
  const file = schemaData?.fetchAPISchema?.file;

  useRegisterRecentVisit({
    isLoading: isLoading || isDecodingFile,
    log: useMemo(
      () => ({
        title: schema?.name || '',
        tool: 'swagger',
        url: pathname,
        id: namespaceSlug as string,
        envSlug: envSlug as string,
      }),
      [pathname, namespaceSlug, schema?.name, envSlug]
    ),
    onRemoveId: namespaceSlug,
  });

  const schemaFile = useMemo(() => {
    if (file) {
      try {
        setIsDecodingFile.on();
        const data = yaml.load(window.atob(file));
        return data as object;
      } catch (error) {
        window.OpNotification.danger({
          subject: 'Failed to parse file!!',
        });
      } finally {
        setIsDecodingFile.off();
      }
    }
    return '';
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [file]);

  if (isLoading || isDecodingFile) {
    return (
      <Bullseye>
        <Spinner size="xl" />
      </Bullseye>
    );
  }

  if (!file) {
    return (
      <Bullseye>
        <EmptyState>
          <EmptyStateIcon icon={CubeIcon} />
          <Title headingLevel="h4" size="lg">
            Sorry, Couldn&apos;t find this API
          </Title>
          <Button variant="primary" onClick={() => navigate('/apis')}>
            Go Back
          </Button>
        </EmptyState>
      </Bullseye>
    );
  }

  return <SwaggerUI spec={schemaFile} tryItOutEnabled />;
}
Example #7
Source File: APICUDPage.tsx    From one-platform with MIT License 4 votes vote down vote up
APICUDPage = () => {
  const [wizardStep, setWizardStep] = useState(1);
  const { handleDynamicCrumbs } = useBreadcrumb();
  const userInfo = opcBase.auth?.getUserInfo();
  const navigate = useNavigate();
  const { slug } = useParams();
  const gqlClient = useURQL();
  const isUpdate = Boolean(slug);

  const [isDeleteConfirmationOpen, setIsDeleteConfirmationOpen] = useToggle();

  // gql queries
  const [createNsState, createNamespace] = useCreateNamespace();
  const [, updateANamespace] = useUpdateNamespace();
  const [deleteNamespaceState, deleteANamespace] = useDeleteANamespace();
  const { isLoading: isNamespaceLoading, data: nsData } = useGetANamespaceBySlug({ slug });

  const namespace = nsData?.getNamespaceBySlug;
  const id = namespace?.id;

  const formMethod = useForm<FormData>({
    defaultValues: FORM_DEFAULT_VALUE,
    mode: 'onBlur',
    reValidateMode: 'onBlur',
    resolver: yupResolver(
      wizardValidationSchemas[wizardStep as keyof typeof wizardValidationSchemas]
    ),
  });

  const { handleSubmit, reset } = formMethod;

  // effect for breadcrumb data
  useEffect(() => {
    if (!isNamespaceLoading && isUpdate && namespace?.name && namespace?.slug) {
      handleDynamicCrumbs({
        'api-name': { label: namespace.name, url: `/apis/${namespace.slug}` },
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isNamespaceLoading, namespace?.name, namespace?.slug, isUpdate]);

  /**
   * This effect validated whether user has edit access if not move them to explore
   * Checks user is in owner list or in createdBy
   */
  useEffect(() => {
    if (!isNamespaceLoading && isUpdate && namespace) {
      const userUuid = userInfo?.rhatUUID;
      const isApiCreatedUser = userUuid === (namespace?.createdBy as UserRoverDetails)?.rhatUUID;
      const isOwner =
        namespace?.owners.findIndex(
          (owner) => owner.group === ApiEmailGroup.USER && owner.user.rhatUUID === userUuid
        ) !== -1;
      const hasEditAccess = isApiCreatedUser || isOwner;
      if (!hasEditAccess) navigate('/apis');
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isNamespaceLoading, namespace, isUpdate, userInfo?.rhatUUID]);

  /**
   * In update mode the form is prefilled with API config data
   */
  useEffect(() => {
    if (!isNamespaceLoading && isUpdate && namespace) {
      const owners = namespace.owners.map((owner) => ({
        group: owner.group,
        mid: owner.group === ApiEmailGroup.USER ? owner?.user?.rhatUUID : owner?.email,
        email: owner.group === ApiEmailGroup.USER ? owner?.user?.mail : owner?.email,
      }));
      reset({
        ...namespace,
        owners,
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isNamespaceLoading, namespace, isUpdate]);

  const isLastStep = wizardStep === MAX_WIZARD_STEPS;

  const formatFormData = ({ id: nsId, slug: nSlug, ...data }: FormData) => {
    return {
      ...data,
      owners: data.owners.map(({ group, mid }) => ({ group, mid })),
      schemas: data.schemas.map((schema) => ({
        ...schema,
        environments: schema.environments.map(({ slug: eSlug, ...env }) => ({
          ...env,
          headers: (env?.headers || [])
            .filter(({ id: hId, key, value }) => (hId && key) || (key && value))
            .map(({ id: hID, key, value }) => (hID ? { id: hID, key } : { key, value })),
        })),
      })),
    };
  };

  const handleCreateNamespace = async (data: FormData) => {
    const payload = formatFormData(data) as CreateNamespaceType;
    try {
      const res = await createNamespace({ payload });
      if (res.error) {
        window.OpNotification.danger({
          subject: 'Failed to create API',
          body: res.error?.message,
        });
        return;
      }
      navigate(`/apis/${res.data?.createNamespace.slug}`);
    } catch (error) {
      window.OpNotification.danger({
        subject: 'Failed to create API',
      });
    }
  };

  const handleUpdateNamespace = async (data: FormData) => {
    const payload = formatFormData(data) as CreateNamespaceType;
    delete (payload as any).createdBy;
    try {
      const res = await updateANamespace({ payload, id: id as string });
      if (res.error) {
        window.OpNotification.danger({
          subject: 'Failed to update API',
          body: res.error?.message,
        });
        return;
      }
      navigate(`/apis/${res.data?.updateNamespace.slug}`);
    } catch (error) {
      window.OpNotification.danger({
        subject: 'Failed to update API',
      });
    }
  };

  const handleSchemaValidation = async ({ envSlug, ...config }: HandleSchemaValidationArg) => {
    try {
      const res = await gqlClient
        .query<UseGetAPISchemaFileQuery, UseGetAPISchemaFileVariable>(GET_API_SCHEMA_FILE, {
          config,
          envSlug,
        })
        .toPromise();

      return res.data?.fetchAPISchema;
    } catch (error) {
      window.OpNotification.danger({
        subject: 'Failed to fetch schema',
      });
    }
    return undefined;
  };

  const handleApiDelete = async (): Promise<void> => {
    if (deleteNamespaceState.fetching) return;
    const res = await deleteANamespace({ id: id as string });
    if (res.error) {
      window.OpNotification.danger({
        subject: `Failed to delete API`,
        body: res.error?.message,
      });
    } else {
      navigate('/apis');
    }
  };

  const onFormSubmit = (data: FormData) => {
    if (wizardStep < MAX_WIZARD_STEPS) {
      setWizardStep((state) => state + 1);
      return;
    }
    if (isUpdate) {
      handleUpdateNamespace(data);
    } else {
      handleCreateNamespace(data);
    }
  };

  const onPrev = () => {
    if (wizardStep > 1) {
      setWizardStep((state) => state - 1);
    }
  };

  const onSearchOwners = async (search: string): Promise<JSX.Element[]> => {
    if (!search || search.length < 3) {
      return [
        <SelectOption key="no-result" value="Please type atleast 3 characters" isPlaceholder />,
      ];
    }

    try {
      const res = await gqlClient.query<UserSearchQuery>(GET_USERS_QUERY, { search }).toPromise();
      const options = (res.data?.searchRoverUsers || []).map((owner) => (
        <SelectOption
          key={`user:${owner.mail}-owner-${owner.rhatUUID}`}
          value={{
            ...owner,
            toString: () => owner.cn,
          }}
          description={owner.mail}
        />
      ));
      return options;
    } catch (error) {
      window.OpNotification.danger({
        subject: 'Failed to search for users',
      });
    }
    return [];
  };

  if (isUpdate && isNamespaceLoading) {
    return (
      <Bullseye>
        <Spinner size="xl" />
      </Bullseye>
    );
  }

  return (
    <PageSection
      isCenterAligned
      isWidthLimited
      style={{ backgroundColor: 'var(--pf-global--BackgroundColor--light-300)' }}
      className="pf-u-h-100 pf-u-pb-4xl"
    >
      <Form
        onSubmit={handleSubmit(onFormSubmit)}
        style={{ maxWidth: '1080px', margin: 'auto' }}
        autoComplete="off"
      >
        <FormProvider {...formMethod}>
          <Stack hasGutter>
            {/* Top Stepper */}
            <StackItem>
              <Card>
                <CardBody>
                  <ProgressStepper isCenterAligned>
                    {wizardStepDetails.map(({ title }, index) => (
                      <ProgressStep
                        variant={wizardStep <= index ? 'pending' : 'success'}
                        id={`wizard-step-icon-${index}`}
                        key={`wizard-step-icon-${index + 1}`}
                        titleId={`wizard-step-icon-${index}`}
                        aria-label={title}
                        isCurrent={wizardStep === index + 1}
                      >
                        {title}
                      </ProgressStep>
                    ))}
                  </ProgressStepper>
                </CardBody>
              </Card>
            </StackItem>
            {/* Form Steps */}
            <StackItem>
              <CSSTransition in={wizardStep === 1} timeout={200} classNames="fade-in" unmountOnExit>
                <APIBasicDetailsForm onSearchOwners={onSearchOwners} />
              </CSSTransition>
            </StackItem>
            <StackItem>
              <CSSTransition in={wizardStep === 2} timeout={200} classNames="fade-in" unmountOnExit>
                <APISchemaForm
                  handleSchemaValidation={handleSchemaValidation}
                  isUpdate={isUpdate}
                />
              </CSSTransition>
            </StackItem>
            <StackItem>
              <CSSTransition in={wizardStep === 3} timeout={200} classNames="fade-in" unmountOnExit>
                <APIReview />
              </CSSTransition>
            </StackItem>
            {/* Form Action Buttons */}
            <StackItem>
              <Card>
                <CardBody>
                  <Split hasGutter>
                    <SplitItem>
                      <Button type="submit" isLoading={createNsState.fetching}>
                        {isLastStep ? (isUpdate ? 'Update' : 'Create') : 'Next'}
                      </Button>
                    </SplitItem>
                    <SplitItem>
                      <Button variant="secondary" onClick={onPrev} isDisabled={wizardStep === 1}>
                        Back
                      </Button>
                    </SplitItem>
                    <SplitItem isFilled>
                      <Link to={isUpdate ? `/apis/${namespace?.slug}` : '/apis'}>
                        <Button variant="link">Cancel</Button>
                      </Link>
                    </SplitItem>
                    {isUpdate && (
                      <SplitItem>
                        <Button variant="link" isDanger onClick={setIsDeleteConfirmationOpen.on}>
                          Delete
                        </Button>
                      </SplitItem>
                    )}
                  </Split>
                </CardBody>
              </Card>
            </StackItem>
          </Stack>
        </FormProvider>
      </Form>
      <Modal
        variant={ModalVariant.medium}
        title={`Delete ${namespace?.name} API`}
        titleIconVariant="danger"
        isOpen={isDeleteConfirmationOpen}
        onClose={setIsDeleteConfirmationOpen.off}
        actions={[
          <Button
            key="confirm"
            variant="primary"
            onClick={handleApiDelete}
            isLoading={deleteNamespaceState.fetching}
          >
            Confirm
          </Button>,
          <Button key="cancel" variant="link" onClick={setIsDeleteConfirmationOpen.off}>
            Cancel
          </Button>,
        ]}
      >
        This action is irreversible. Are you sure you want to delete this API?
      </Modal>
    </PageSection>
  );
}
Example #8
Source File: EnvSchemaField.tsx    From one-platform with MIT License 4 votes vote down vote up
EnvSchemaField = forwardRef<HTMLInputElement, Props>(
  (
    { isGraphqlAPI, isError, errorMessage, envIndex, onCopyValue, onRedoValidation, ...inputProps },
    ref
  ): JSX.Element => {
    const [isValidating, setIsValidating] = useToggle();

    const handleRedoValidation = async () => {
      setIsValidating.on();
      await onRedoValidation();
      setIsValidating.off();
    };

    return (
      <FormGroup
        fieldId={`environments.${envIndex}.schemaEndpoint`}
        label={isGraphqlAPI ? 'Introspection URL' : 'API Schema URL'}
        validated={isError ? 'error' : 'success'}
        helperTextInvalid={errorMessage}
        labelIcon={
          isGraphqlAPI ? (
            <Popover
              headerContent={<div>What is Introsepection URL?</div>}
              bodyContent={
                <div>
                  GraphQL schema for information about what queries it supports. GraphQL allows us
                  to do so using the introspection system. For more information visit:{' '}
                  <a
                    href="https://graphql.org/learn/introspection/"
                    target="_blank"
                    rel="noreferrer noopener"
                  >
                    Doc
                  </a>
                </div>
              }
            >
              <button
                type="button"
                aria-label="More info for name field"
                onClick={(e) => e.preventDefault()}
                aria-describedby="simple-form-name-01"
                className="pf-c-form__group-label-help"
              >
                <HelpIcon noVerticalAlign />
              </button>
            </Popover>
          ) : undefined
        }
        helperText={
          isGraphqlAPI &&
          !inputProps.value && (
            <Button
              variant="plain"
              className="pf-u-font-size-xs pf-u-px-0 pf-u-mt-xs"
              onClick={onCopyValue}
            >
              Click here to use API Base Path as introspection URL
            </Button>
          )
        }
      >
        <Split>
          <SplitItem isFilled>
            <TextInput
              aria-label="env link"
              placeholder="Enter api schema URL"
              ref={ref}
              {...inputProps}
              onBlur={(event) => {
                handleRedoValidation();
                if (inputProps?.onBlur) inputProps?.onBlur(event);
              }}
            />
          </SplitItem>
          <SplitItem>
            <Button variant="control" onClick={handleRedoValidation}>
              {isValidating ? <Spinner size="sm" /> : <UndoIcon />}
            </Button>
          </SplitItem>
        </Split>
      </FormGroup>
    );
  }
)
Example #9
Source File: APIDescriptionPage.tsx    From one-platform with MIT License 4 votes vote down vote up
APIDescriptionPage = (): JSX.Element => {
  const { slug } = useParams();
  const navigate = useNavigate();
  const { handleDynamicCrumbs } = useBreadcrumb();
  const urlParser = useURLParser();
  const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(0);
  const [isSubscriptionOptionOpen, setIsSubscriptionOptionOpen] = useToggle();
  const [isSchemaDropdownOpen, setIsSchemaDropdownOpen] = useToggle();
  const [selectedSubscriptonEnv, setSelectedSubscriptionEnv] = useState<Record<
    string,
    boolean
  > | null>(null);
  const userInfo = opcBase.auth?.getUserInfo();

  const [{ fetching: isSubscribing, data: subscribedNamespace }, handleSubscribeSchemaGQL] =
    useSubscribeSchema();
  const { isLoading: isNamespaceLoading, data: fetchedNamespace } = useGetANamespaceBySlug({
    slug,
  });

  const namespace = subscribedNamespace?.subscribeApiSchema || fetchedNamespace?.getNamespaceBySlug;
  const id = namespace?.id;
  const schemas = namespace?.schemas || [];
  const selectedSchema = namespace?.schemas[selectedSchemaIndex];

  // effect to add breadcrumb data
  useEffect(() => {
    if (!isNamespaceLoading && namespace?.name && namespace?.id) {
      handleDynamicCrumbs({
        'api-name': { label: namespace.name, url: `/apis/${namespace?.slug}` },
      });
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isNamespaceLoading, namespace?.name, namespace?.id]);

  const hasEditAccess = useMemo(() => {
    const userUuid = userInfo?.rhatUUID;
    return hasUserApiEditAccess(userUuid as string, namespace);
  }, [namespace, userInfo?.rhatUUID]);

  const onMenuClick = (schemaId: string) => {
    const index = namespace?.schemas.findIndex(({ id: sid }) => sid === schemaId);
    if (index !== -1) {
      setSelectedSchemaIndex(index || 0);
    }
    setIsSchemaDropdownOpen.off();
  };

  const hasSubscribed = selectedSchema?.environments.some(({ isSubscribed }) => isSubscribed);

  const handleSchemaSubscription = async (envIDs?: string[]) => {
    const subscribedIds =
      envIDs ||
      (hasSubscribed ? [] : (selectedSchema?.environments || []).map(({ id: sID }) => sID));
    const subscriptionConfig = {
      namespaceID: id as string,
      schemaID: selectedSchema?.id || '',
      envIDs: subscribedIds,
      email: userInfo?.email || '',
    };
    try {
      const res = await handleSubscribeSchemaGQL({ config: subscriptionConfig });
      if (res.error) {
        opcBase.toast.danger({
          subject: `Failed to ${hasSubscribed ? 'unsubscribe' : 'subscribe'} api`,
          body: res?.error?.message,
        });
      } else {
        const subject = `${hasSubscribed ? 'Unsubscribed' : 'Subscribed'} to ${namespace?.name}`;
        const body = `You will ${
          hasSubscribed ? 'not be' : 'be'
        } be notified regarding updates on this API`;
        opcBase.toast.info({ subject, body });
      }
    } catch (error) {
      opcBase.toast.danger({
        subject: `Failed to ${hasSubscribed ? 'unsubscribe' : 'subscribe'} api`,
      });
    }
  };

  const onInitializeSelect = () => {
    if (!isSubscriptionOptionOpen) {
      const alreadySubscripedEnv = (selectedSchema?.environments || []).reduce<
        Record<string, boolean>
      >(
        (prev, { isSubscribed, id: eId }) =>
          isSubscribed ? { ...prev, [eId]: true } : { ...prev },
        {}
      );
      setSelectedSubscriptionEnv(alreadySubscripedEnv);
    }
    setIsSubscriptionOptionOpen.toggle();
  };

  const onEnvSelect = (env: string) => {
    const isEnvPresent = Boolean(selectedSubscriptonEnv?.[env]);
    const state = { ...selectedSubscriptonEnv };
    if (isEnvPresent) {
      delete state[env];
      setSelectedSubscriptionEnv(state);
    } else {
      state[env] = true;
      setSelectedSubscriptionEnv(state);
    }
  };

  const onEnvSelectBlur = async () => {
    if (selectedSubscriptonEnv) {
      await handleSchemaSubscription(Object.keys(selectedSubscriptonEnv));
    }
    setIsSubscriptionOptionOpen.off();
    setSelectedSubscriptionEnv(null);
  };

  if (isNamespaceLoading) {
    return (
      <Bullseye>
        <Spinner size="xl" />
      </Bullseye>
    );
  }

  if (!namespace) {
    return (
      <Bullseye>
        <EmptyState>
          <EmptyStateIcon icon={CubesIcon} />
          <Title headingLevel="h4" size="lg">
            Sorry, Couldn&apos;t find this API
          </Title>
          <Button variant="primary" onClick={() => navigate('../')}>
            Go Back
          </Button>
        </EmptyState>
      </Bullseye>
    );
  }

  return (
    <Stack>
      <StackItem>
        <PageSection isWidthLimited isCenterAligned>
          <Grid hasGutter>
            <GridItem span={8}>
              <DetailsSection namespace={namespace} id={slug} hasEditAccess={hasEditAccess} />
            </GridItem>
            <GridItem span={4}>
              <ApiSchemaList
                schemas={namespace?.schemas}
                onClick={onMenuClick}
                selectedSchemaID={selectedSchema?.id}
              />
            </GridItem>
          </Grid>
        </PageSection>
      </StackItem>
      <StackItem>
        <PageSection
          isWidthLimited
          isCenterAligned
          padding={{ default: 'noPadding' }}
          className="pf-u-py-sm pf-u-px-md"
        >
          <Text component={TextVariants.small} className="pf-u-color-400">
            API Schema
          </Text>
        </PageSection>
      </StackItem>
      <StackItem>
        <Divider />
      </StackItem>
      <StackItem>
        <PageSection isWidthLimited isCenterAligned className="pf-u-pb-4xl">
          <Grid hasGutter>
            {selectedSchema?.flags.isDeprecated && (
              <Grid span={12}>
                <Alert variant="danger" isInline title={`${selectedSchema.name} is deprecated`} />
              </Grid>
            )}
            <GridItem span={8}>
              <Stack
                hasGutter
                style={{ '--pf-l-stack--m-gutter--MarginBottom': '1.5rem' } as CSSProperties}
              >
                <StackItem className={styles.schemaContainer}>
                  <Split>
                    <SplitItem isFilled>
                      <Button
                        variant="link"
                        icon={<CaretDownIcon />}
                        onClick={setIsSchemaDropdownOpen.toggle}
                        iconPosition="right"
                        style={{ color: 'black' }}
                        className={styles.schemaDropdownTitle}
                      >
                        {selectedSchema?.name}
                      </Button>
                    </SplitItem>
                    <SplitItem className="pf-u-mr-lg">
                      <Label color={selectedSchema?.flags?.isInternal ? 'blue' : 'green'} isCompact>
                        {selectedSchema?.flags?.isInternal ? 'Internal API' : 'External API'}
                      </Label>
                    </SplitItem>
                  </Split>
                  <CSSTransition
                    in={isSchemaDropdownOpen}
                    timeout={200}
                    classNames="fade-in"
                    unmountOnExit
                  >
                    <Menu className={styles.schemaMenu}>
                      <MenuContent>
                        <MenuList className="pf-u-py-0">
                          {schemas.map((schema, index) => (
                            <Fragment key={schema.id}>
                              <MenuItem
                                className={css({
                                  'menu-selected': schema.id === selectedSchema?.id,
                                })}
                                icon={
                                  <Avatar
                                    src={`${config.baseURL}/images/${
                                      schema.category === 'REST'
                                        ? 'swagger-black-logo.svg'
                                        : 'graphql-logo.svg'
                                    }`}
                                    alt="api-type"
                                    size="sm"
                                    style={{ width: '1.25rem', height: '1.25rem' }}
                                    className="pf-u-mt-sm"
                                  />
                                }
                                onClick={() => onMenuClick(schema.id)}
                              >
                                <Split>
                                  <SplitItem isFilled>{schema.name}</SplitItem>
                                  <SplitItem>
                                    <Label
                                      color={schema.flags.isInternal ? 'blue' : 'green'}
                                      isCompact
                                      className="pf-u-ml-sm"
                                    >
                                      {schema.flags.isInternal ? 'Internal' : 'External'}
                                    </Label>
                                  </SplitItem>
                                </Split>
                              </MenuItem>
                              {schemas.length - 1 !== index && (
                                <Divider component="li" className="pf-u-my-0" />
                              )}
                            </Fragment>
                          ))}
                        </MenuList>
                      </MenuContent>
                    </Menu>
                  </CSSTransition>
                </StackItem>
                <StackItem>
                  <ReadMore>{selectedSchema?.description || ''}</ReadMore>
                </StackItem>
                <StackItem>
                  <Split hasGutter>
                    <SplitItem isFilled>
                      <Title headingLevel="h3">Application URL</Title>
                      <a href={selectedSchema?.appURL} target="_blank" rel="noopener noreferrer">
                        <Text className="pf-u-color-400">
                          {urlParser(selectedSchema?.appURL || '')}
                        </Text>
                      </a>
                    </SplitItem>
                    <SplitItem isFilled>
                      <Title headingLevel="h3">Documentation URL</Title>
                      <a href={selectedSchema?.docURL} target="_blank" rel="noopener noreferrer">
                        <Text className="pf-u-color-400">
                          {urlParser(selectedSchema?.docURL || '')}
                        </Text>
                      </a>
                    </SplitItem>
                  </Split>
                </StackItem>
                <StackItem className="pf-u-mt-md">
                  <ApiEnvironmentSection
                    environments={selectedSchema?.environments}
                    category={selectedSchema?.category}
                  />
                </StackItem>
              </Stack>
            </GridItem>
            <GridItem span={1} />
            <GridItem span={3}>
              <Stack hasGutter>
                <StackItem className={styles.subscriptionContainer}>
                  <Split>
                    <SplitItem isFilled>
                      <Button
                        icon={<BellIcon />}
                        variant={hasSubscribed ? 'primary' : 'secondary'}
                        iconPosition="right"
                        isBlock
                        isLoading={isSubscribing}
                        className={css(hasSubscribed ? styles.subscriptionDropdownBtn : null)}
                        onClick={() => handleSchemaSubscription()}
                      >
                        {hasSubscribed ? 'Subscribed' : 'Subscribe'}
                      </Button>
                    </SplitItem>
                    <CSSTransition
                      in={hasSubscribed}
                      timeout={200}
                      classNames="fade-in"
                      unmountOnExit
                    >
                      <SplitItem>
                        <Button
                          icon={<CaretDownIcon />}
                          onClick={onInitializeSelect}
                          className={css('ignore-blur', styles.subscriptionDropdownArrow)}
                        />
                      </SplitItem>
                    </CSSTransition>
                  </Split>
                  <CSSTransition
                    in={isSubscriptionOptionOpen}
                    timeout={200}
                    classNames="fade-in"
                    unmountOnExit
                  >
                    <Menu
                      className={styles.subscriptionMenu}
                      onBlur={(e) => {
                        if (!e.relatedTarget?.className?.includes('ignore-blur')) {
                          onEnvSelectBlur();
                        }
                      }}
                    >
                      <MenuContent>
                        <MenuList className="pf-u-py-0">
                          {selectedSchema?.environments.map(({ name, isSubscribed, id: envId }) => (
                            <MenuItem
                              className="uppercase ignore-blur"
                              isSelected={
                                selectedSubscriptonEnv
                                  ? selectedSubscriptonEnv[envId]
                                  : isSubscribed
                              }
                              key={`subscription-${envId}`}
                              itemId={envId}
                              onClick={() => onEnvSelect(envId)}
                            >
                              {name}
                            </MenuItem>
                          ))}
                        </MenuList>
                      </MenuContent>
                    </Menu>
                  </CSSTransition>
                </StackItem>
                <StackItem>
                  <ApiTypeCard category={selectedSchema?.category} />
                </StackItem>
              </Stack>
            </GridItem>
          </Grid>
        </PageSection>
      </StackItem>
    </Stack>
  );
}
Example #10
Source File: APIListPage.tsx    From one-platform with MIT License 4 votes vote down vote up
APIListPage = (): JSX.Element => {
  const navigate = useNavigate();

  // query param strings
  const query = useQueryParams();
  const mid = query.get('mid');
  const defaultSearch = query.get('search');

  // filters, search, sorting
  const [isSortSelectOpen, setSortSelect] = useToggle();
  const [sortOption, setSortOption] = useState(SortBy.RECENTLY_ADDED);
  const [filters, setFilters] = useState<{ type: null | ApiCategory; search: string }>({
    type: null,
    search: defaultSearch || '',
  });
  const { pagination, onPerPageSelect, onSetPage, onResetPagination } = usePagination({
    page: 1,
    perPage: 20,
  });
  const debouncedSearch = useDebounce(filters.search);

  // graphql query hooks
  const { isLoading: isApiListLoading, data: namespaceList } = useGetNamespaceList({
    limit: pagination.perPage,
    offset: (pagination.page - 1) * pagination.perPage,
    apiCategory: filters.type,
    search: debouncedSearch,
    sortBy: sortOption === SortBy.RECENTLY_ADDED ? 'CREATED_ON' : 'UPDATED_ON',
    mid,
  });
  const { isLoading: isNamespaceStatLoading, data: namespaceStats } = useGetNamespaceStats({
    search: debouncedSearch,
    mid,
  });

  const handleApiOwnersRender = useCallback((owners: ApiOwnerType[]) => {
    return owners.map((owner) =>
      owner.group === ApiEmailGroup.USER ? owner.user.cn : owner.email
    );
  }, []);

  const onStatCardClick = (cardType: 'total' | 'rest' | 'graphql') => {
    onResetPagination();
    if (cardType === 'total') {
      setFilters((state) => ({ ...state, type: null }));
    } else {
      setFilters((state) => ({ ...state, type: cardType.toUpperCase() as ApiCategory }));
    }
  };

  const onSearch = (search: string) => {
    setFilters((state) => ({ ...state, search }));
  };

  const onSortSelect = (
    event: React.MouseEvent | React.ChangeEvent,
    value: string | SelectOptionObject,
    isPlaceholder?: boolean
  ) => {
    if (isPlaceholder) setSortOption(SortBy.RECENTLY_ADDED);
    else setSortOption(value as SortBy);

    setSortSelect.off(); // close the select
  };

  const onCardClick = (id: string) => {
    navigate(id);
  };

  const namespaceCount = namespaceStats?.getApiCategoryCount;
  const namespaces = namespaceList?.listNamespaces?.data;

  const isNamespaceEmpty = !isApiListLoading && namespaces?.length === 0;

  return (
    <>
      <Header />
      <Divider />
      <PageSection variant="light" isWidthLimited className="pf-m-align-center">
        <Grid hasGutter>
          <Grid hasGutter span={12}>
            {stats.map(({ key, type, image }) => (
              <GridItem
                key={`api-select-${type}`}
                span={4}
                className={styles['api-list--stat-card']}
                type={key}
              >
                <StatCard
                  value={namespaceCount?.[key]}
                  category={type}
                  isLoading={isNamespaceStatLoading}
                  onClick={callbackify(onStatCardClick, key)}
                  isSelected={filters.type ? filters.type.toLowerCase() === key : key === 'total'}
                >
                  <img
                    src={`${config.baseURL}/images/${image}`}
                    alt={`api-select-${type}`}
                    style={{ height: '48px' }}
                  />
                </StatCard>
              </GridItem>
            ))}
          </Grid>
          <GridItem className="pf-u-my-md">
            <Split hasGutter className={styles['api-list--table-filter--container']}>
              <SplitItem isFilled>
                <Link to={ApiCatalogLinks.AddNewApiPage}>
                  <Button>Add API</Button>
                </Link>
              </SplitItem>
              <SplitItem className="pf-u-w-33">
                <Form>
                  <FormGroup fieldId="search">
                    <TextInput
                      aria-label="Search API"
                      placeholder="Search for APIs"
                      type="search"
                      iconVariant="search"
                      value={filters.search}
                      onChange={onSearch}
                    />
                  </FormGroup>
                </Form>
              </SplitItem>
              <SplitItem style={{ width: '180px' }}>
                <Select
                  isOpen={isSortSelectOpen}
                  onToggle={setSortSelect.toggle}
                  selections={sortOption}
                  onSelect={onSortSelect}
                >
                  {[
                    <SelectOption key="select-sort-placeholder" value="Sort by" isDisabled />,
                    <SelectOption
                      key={`select-sort:${SortBy.RECENTLY_ADDED}`}
                      value={SortBy.RECENTLY_ADDED}
                    />,
                    <SelectOption
                      key={`select-sort:${SortBy.RECENTLY_MODIFIED}`}
                      value={SortBy.RECENTLY_MODIFIED}
                    />,
                  ]}
                </Select>
              </SplitItem>
            </Split>
          </GridItem>

          {isApiListLoading ? (
            <Bullseye className="pf-u-mt-lg">
              <Spinner size="xl" />
            </Bullseye>
          ) : (
            namespaces?.map(({ id, name, updatedOn, owners, schemas, slug }) => (
              <GridItem
                span={12}
                key={id}
                className="catalog-nav-link"
                onClick={callbackify(onCardClick, slug)}
              >
                <ApiDetailsCard
                  title={name}
                  owners={handleApiOwnersRender(owners)}
                  updatedAt={updatedOn}
                  schemas={schemas.map(({ name: schemaName, category }) => ({
                    name: schemaName,
                    type: category,
                  }))}
                />
              </GridItem>
            ))
          )}
          {isNamespaceEmpty && (
            <EmptyState>
              <EmptyStateIcon icon={CubesIcon} />
              <Title headingLevel="h4" size="lg">
                No API found
              </Title>
              <EmptyStateBody>Add an API to fill this gap</EmptyStateBody>
            </EmptyState>
          )}
        </Grid>
      </PageSection>
      <PageSection variant="light" isWidthLimited className="pf-m-align-center pf-u-pb-2xl">
        <Pagination
          itemCount={namespaceList?.listNamespaces?.count || 0}
          widgetId="pagination-options-menu-bottom"
          perPage={pagination.perPage}
          page={pagination.page}
          onSetPage={(_, page) => onSetPage(page)}
          onPerPageSelect={(_, perPage) => onPerPageSelect(perPage)}
          isCompact
        />
      </PageSection>
    </>
  );
}
Example #11
Source File: GQLPlaygroundToolboxPage.tsx    From one-platform with MIT License 4 votes vote down vote up
GQLPlaygroundToolboxPage = (): JSX.Element => {
  const { envSlug } = useParams();
  const navigate = useNavigate();
  const { pathname } = useLocation();
  const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
  const [isDecodingFile, setIsDecodingFile] = useToggle();

  const schema = schemaData?.fetchAPISchema?.schema;
  const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
  const file = schemaData?.fetchAPISchema?.file;

  useRegisterRecentVisit({
    isLoading: isLoading || isDecodingFile,
    log: useMemo(
      () => ({
        title: schema?.name || '',
        tool: 'playground',
        url: pathname,
        id: namespaceSlug as string,
        envSlug: envSlug as string,
      }),
      [pathname, namespaceSlug, schema?.name, envSlug]
    ),
    onRemoveId: namespaceSlug,
  });

  const env = useMemo(() => {
    if (schema) {
      return schema.environments.find(({ slug }) => slug === envSlug);
    }
    return { apiBasePath: '' };
  }, [schema, envSlug]);

  const schemaFile = useMemo(() => {
    if (file) {
      try {
        setIsDecodingFile.on();
        const data = JSON.parse(window.atob(file));
        return data.data as object;
      } catch (error) {
        window.OpNotification.danger({
          subject: 'Failed to parse file!!',
        });
      } finally {
        setIsDecodingFile.off();
      }
    }
    return '';
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [file]);

  if (isLoading || isDecodingFile) {
    return (
      <Bullseye>
        <Spinner size="xl" />
      </Bullseye>
    );
  }

  if (!file) {
    return (
      <Bullseye>
        <EmptyState>
          <EmptyStateIcon icon={CubeIcon} />
          <Title headingLevel="h4" size="lg">
            Sorry, Couldn&apos;t find this API
          </Title>
          <Button variant="primary" onClick={() => navigate('/apis')}>
            Go Back
          </Button>
        </EmptyState>
      </Bullseye>
    );
  }

  return (
    <Provider store={store}>
      <Playground
        endpoint={env?.apiBasePath}
        schema={schemaFile}
        settings={{
          'editor.theme': 'light',
          'schema.enablePolling': false,
        }}
      />
    </Provider>
  );
}
Example #12
Source File: RedocToolboxPage.tsx    From one-platform with MIT License 4 votes vote down vote up
RedocToolboxPage = (): JSX.Element => {
  const { envSlug } = useParams();
  const navigate = useNavigate();
  const { pathname } = useLocation();
  const { isLoading, data: schemaData } = useGetApiSchemaFile({ envSlug });
  const [isDecodingFile, setIsDecodingFile] = useToggle();
  const redocContainer = useRef<HTMLDivElement>(null);

  const schema = schemaData?.fetchAPISchema?.schema;
  const namespaceSlug = schemaData?.fetchAPISchema?.namespaceSlug;
  const file = schemaData?.fetchAPISchema?.file;

  useEffect(() => {
    loadThirdPartyScript(
      'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js',
      'redoc-script',
      () => {
        window.process = { ...(window.process || {}), cwd: () => '' };
      },
      () =>
        window.OpNotification.danger({
          subject: 'Failed to load redoc',
        })
    );
  }, []);

  useEffect(() => {
    if (file && typeof Redoc !== 'undefined') {
      try {
        setIsDecodingFile.on();
        const data = yaml.load(window.atob(file));
        Redoc.init(data, {}, document.getElementById('redoc-container'));
      } catch (error) {
        window.OpNotification.danger({
          subject: 'Failed to parse file!!',
        });
      } finally {
        setIsDecodingFile.off();
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [file, typeof Redoc]);

  useRegisterRecentVisit({
    isLoading: isLoading || isDecodingFile,
    log: useMemo(
      () => ({
        title: schema?.name || '',
        tool: 'redoc',
        url: pathname,
        id: namespaceSlug as string,
        envSlug: envSlug as string,
      }),
      [pathname, namespaceSlug, schema?.name, envSlug]
    ),
    onRemoveId: namespaceSlug,
  });

  if (isLoading || isDecodingFile) {
    return (
      <Bullseye>
        <Spinner size="xl" />
      </Bullseye>
    );
  }

  if (!file) {
    return (
      <Bullseye>
        <EmptyState>
          <EmptyStateIcon icon={CubeIcon} />
          <Title headingLevel="h4" size="lg">
            Sorry, Couldn&apos;t find this API
          </Title>
          <Button variant="primary" onClick={() => navigate('/apis')}>
            Go Back
          </Button>
        </EmptyState>
      </Bullseye>
    );
  }

  return <div id="redoc-container" ref={redocContainer} />;
}
Example #13
Source File: HomePage.tsx    From one-platform with MIT License 4 votes vote down vote up
HomePage = (): JSX.Element => {
  const query = useQueryParams();
  // modal state hooks
  const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
    { name: 'detailView', isOpen: Boolean(query.get('id')) },
    { name: 'appList', isOpen: false },
  ]);

  // hooks for api filtering, pagination
  const [filters, setFilters] = useState<FeedbackFilters>({
    selectedApps: null,
    status: null,
    category: null,
    search: '',
  });
  const { pagination, onPerPageSelect, onSetPage } = usePagination();
  const [isMyFeedback, setIsMyFeedback] = useToggle();
  const [isExporting, setIsExporting] = useToggle();

  const debouncedSearch = useDebounce(filters.search, 500);

  // convert the filters to graphl variable format for api calls
  const queryFilters = useMemo<GetFeedbackListQueryVariables>(() => {
    const appIds = Object.keys(filters.selectedApps || {});
    const userInfo = opcBase.auth?.getUserInfo();
    return {
      ...filters,
      search: debouncedSearch,
      appId: appIds.length !== 0 ? appIds : null,
      createdBy: isMyFeedback ? userInfo?.rhatUUID : null,
      limit: pagination.perPage,
      offset: (pagination.page - 1) * pagination.perPage,
    };
  }, [filters, debouncedSearch, isMyFeedback, pagination]);

  // Get app list
  const [{ data: appList, fetching: isAppListLoading }] = useQuery<GetAppsQuery>({
    query: GetApps,
  });

  /**
   * This query will be executed only when there is ?id=<feedback_id>
   * To fetch a particular feedback
   */
  const [{ data: feedbackById, fetching: isFeedbackByIdQueryLoading }] = useQuery<
    GetFeedbackById,
    { id: string }
  >({
    query: GetFeedback,
    variables: { id: query?.get('id') as string },
    pause: !query?.get('id'),
  });

  // Get feedback list
  const [{ data: fetchedFeedback, fetching: isFeedbackListLoading }] = useQuery<
    GetFeedbackListQuery,
    GetFeedbackListQueryVariables
  >({
    query: GetFeedbackList,
    variables: queryFilters,
  });

  const feedbacks = fetchedFeedback?.listFeedbacks;
  const selectedFeedback = (popUp?.detailView?.data as Feedback) || feedbackById?.getFeedbackById;

  // To keep all selectedApps sorted to top
  const selectedApps = useMemo(() => {
    if (!appList?.apps) return [];
    const apps = [...appList.apps];
    return apps
      .sort(({ id }) => (filters.selectedApps?.[id] ? -1 : 1)) // sort it with selected apps at top
      .slice(0, Math.max(Object.keys(filters.selectedApps || {}).length, 5)); // either show 5 apps or all the selected ones
  }, [appList?.apps, filters.selectedApps]);

  /**
   * To show the title of the ticket created
   * Computes it based on JIRA, Github, Gitlab
   */
  const formatePopupTitle = useCallback((type: string, url: string) => {
    if (!type || !url) {
      return '';
    }
    const splittedUrl = url.split('/');
    const ticketName = splittedUrl[splittedUrl.length - 1];
    if (type.toLowerCase() === 'jira') {
      return ticketName;
    }
    return `${type} ${ticketName}`;
  }, []);

  const handleFeedbackFilterChange = useCallback(
    <T extends unknown>(field: keyof FeedbackFilters, value: T) => {
      setFilters((state) => ({ ...state, [field]: value }));
    },
    []
  );

  const handleFeedbackFilterAppIdChange = useCallback(
    (app: App) => {
      const appsSelected = { ...filters.selectedApps };
      if (appsSelected?.[app.id]) {
        delete appsSelected[app.id];
      } else {
        appsSelected[app.id] = app;
      }
      setFilters((state) => ({ ...state, selectedApps: appsSelected }));
    },
    [filters.selectedApps]
  );

  const handleFeedbackFilterClear = useCallback((field: keyof FeedbackFilters) => {
    setFilters((state) => ({ ...state, [field]: null }));
  }, []);

  const onExportToCSV = () => {
    if (!feedbacks?.data) return;
    setIsExporting.on();
    /**
     * Format the feedback list json response for the csv
     * Set the headers and pick required fields only
     */
    const formatedFeedbacks = feedbacks?.data.map((feedback: Record<string, unknown>) => {
      const formatedFeedback: Record<string, unknown> = {};
      EXPORT_FEEDBACK_CSV.forEach(({ title, field }) => {
        const value = field.split('.').reduce((obj, i) => obj[i] as any, feedback);
        formatedFeedback[title] = value;
      });
      return formatedFeedback;
    });
    jsonexport(formatedFeedbacks, (err, csv) => {
      setIsExporting.off();
      if (err) {
        opcBase.toast.danger({ subject: 'Failed to export csv' });
      } else {
        // export to csv
        let csvContent = 'data:text/csv;charset=utf-8,';
        csvContent += csv;
        const encodedCsv = encodeURI(csvContent);
        const link = document.createElement('a');
        link.setAttribute('href', encodedCsv);
        link.setAttribute('download', 'Feedback.csv');
        link.click();
        opcBase.toast.success({
          subject: 'Export sucessfully completed',
        });
      }
    });
  };

  return (
    <>
      <PageSection isWidthLimited variant="light" className=" pf-m-align-center">
        <Grid hasGutter style={{ '--pf-l-grid--m-gutter--GridGap': '2rem' } as CSSProperties}>
          <GridItem span={3}>
            <SearchInput
              type="search"
              id="search-feedback"
              placeholder="Search via name"
              value={filters.search || ''}
              onChange={(value) => handleFeedbackFilterChange('search', value)}
            />
          </GridItem>
          <GridItem span={9}>
            <Split>
              <SplitItem isFilled>
                <Button variant="primary" onClick={setIsMyFeedback.toggle}>
                  {`${isMyFeedback ? 'All' : 'My'} Feedback`}
                </Button>
              </SplitItem>
              <SplitItem>
                <Button
                  icon={<UploadIcon />}
                  variant="secondary"
                  onClick={onExportToCSV}
                  isLoading={isExporting}
                  isDisabled={!feedbacks?.count}
                >
                  Export
                </Button>
              </SplitItem>
            </Split>
          </GridItem>
          <GridItem span={3}>
            <form>
              <Stack hasGutter>
                <StackItem>
                  <Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
                    <StackItem>
                      <FilterTitle
                        title="Applications"
                        onClear={() => handleFeedbackFilterClear('selectedApps')}
                        isClearable={Boolean(filters.selectedApps)}
                      />
                    </StackItem>
                    {isAppListLoading ? (
                      <Bullseye>
                        <Spinner size="lg" label="Loading..." />
                      </Bullseye>
                    ) : (
                      <>
                        {selectedApps.map((app) => (
                          <StackItem key={app.id}>
                            <Checkbox
                              id={app.id}
                              label={app.name}
                              className="capitalize"
                              isChecked={Boolean(filters.selectedApps?.[app.id])}
                              onChange={() => handleFeedbackFilterAppIdChange(app)}
                            />
                          </StackItem>
                        ))}
                        {(appList?.apps || [])?.length > 5 && (
                          <StackItem>
                            <Button
                              variant="link"
                              icon={<PlusIcon />}
                              onClick={() => handlePopUpOpen('appList')}
                            >
                              Expand to see more apps
                            </Button>
                          </StackItem>
                        )}
                      </>
                    )}
                  </Stack>
                </StackItem>
                <StackItem>
                  <Divider />
                </StackItem>
                <StackItem>
                  <Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
                    <StackItem>
                      <FilterTitle
                        title="Type"
                        onClear={() => handleFeedbackFilterClear('category')}
                        isClearable={Boolean(filters.category)}
                      />
                    </StackItem>
                    <StackItem>
                      <Radio
                        id="feedback-type-1"
                        label="Bug"
                        name="type"
                        isChecked={filters?.category === FeedbackCategoryAPI.BUG}
                        onChange={() =>
                          handleFeedbackFilterChange('category', FeedbackCategoryAPI.BUG)
                        }
                      />
                    </StackItem>
                    <StackItem>
                      <Radio
                        id="feedback-type-2"
                        label="Feedback"
                        name="type"
                        isChecked={filters?.category === FeedbackCategoryAPI.FEEDBACK}
                        onChange={() =>
                          handleFeedbackFilterChange('category', FeedbackCategoryAPI.FEEDBACK)
                        }
                      />
                    </StackItem>
                  </Stack>
                </StackItem>
                <StackItem>
                  <Divider />
                </StackItem>
                <StackItem>
                  <Stack hasGutter style={{ '--pf-global--gutter': '0.75rem' } as CSSProperties}>
                    <StackItem>
                      <FilterTitle
                        title="Status"
                        onClear={() => handleFeedbackFilterClear('status')}
                        isClearable={Boolean(filters.status)}
                      />
                    </StackItem>
                    <StackItem>
                      <Radio
                        id="feedback-status-1"
                        label="Open"
                        name="status"
                        isChecked={filters?.status === FeedbackStatusAPI.OPEN}
                        onChange={() =>
                          handleFeedbackFilterChange('status', FeedbackStatusAPI.OPEN)
                        }
                      />
                    </StackItem>
                    <StackItem>
                      <Radio
                        id="feedback-status-2"
                        label="Closed"
                        name="status"
                        isChecked={filters?.status === FeedbackStatusAPI.CLOSED}
                        onChange={() =>
                          handleFeedbackFilterChange('status', FeedbackStatusAPI.CLOSED)
                        }
                      />
                    </StackItem>
                  </Stack>
                </StackItem>
                <StackItem>
                  <Divider />
                </StackItem>
              </Stack>
            </form>
          </GridItem>
          <GridItem span={9}>
            <Stack hasGutter>
              {isFeedbackListLoading || feedbacks?.count === 0 ? (
                <EmptyState>
                  <EmptyStateIcon
                    variant={isFeedbackListLoading ? 'container' : 'icon'}
                    component={isFeedbackListLoading ? Spinner : undefined}
                    icon={CubesIcon}
                  />
                  <Title size="lg" headingLevel="h4">
                    {isFeedbackListLoading ? 'Loading' : 'No feedback found!!'}
                  </Title>
                </EmptyState>
              ) : (
                feedbacks?.data?.map((feedback) => (
                  <StackItem key={feedback.id}>
                    <FeedbackCard
                      title={(feedback.createdBy as FeedbackUserProfileAPI)?.cn}
                      createdOn={feedback.createdOn}
                      description={feedback.summary}
                      experience={feedback.experience}
                      error={feedback.error}
                      module={feedback.module}
                      category={feedback.category}
                      state={feedback.state}
                      onClick={() => handlePopUpOpen('detailView', feedback)}
                    />
                  </StackItem>
                ))
              )}
              <StackItem>
                <Pagination
                  itemCount={feedbacks?.count}
                  isCompact
                  perPage={pagination.perPage}
                  page={pagination.page}
                  onSetPage={(_evt, newPage) => onSetPage(newPage)}
                  widgetId="feedback-pagination"
                  onPerPageSelect={(_evt, perPage) => onPerPageSelect(perPage)}
                />
              </StackItem>
            </Stack>
          </GridItem>
        </Grid>
      </PageSection>
      <Modal
        variant={ModalVariant.small}
        title={formatePopupTitle(selectedFeedback?.source, selectedFeedback?.ticketUrl)}
        isOpen={popUp.detailView.isOpen}
        onClose={() => handlePopUpClose('detailView')}
        footer={
          <Split hasGutter style={{ width: '100%' }}>
            <SplitItem isFilled>
              <a href={selectedFeedback?.ticketUrl} target="_blank" rel="noopener noreferrer">
                <Button isSmall key="more" variant="danger">
                  See {selectedFeedback?.source} Issue
                </Button>
              </a>
            </SplitItem>
            <SplitItem>
              <Button
                key="close"
                variant="tertiary"
                isSmall
                onClick={() => handlePopUpClose('detailView')}
              >
                Close
              </Button>
            </SplitItem>
          </Split>
        }
      >
        <FeedbackDetailCard feedback={selectedFeedback} isLoading={isFeedbackByIdQueryLoading} />
      </Modal>
      <Modal
        variant={ModalVariant.medium}
        title="All Applications"
        isOpen={popUp.appList.isOpen}
        onClose={() => handlePopUpClose('appList')}
      >
        <AppListCard
          apps={appList?.apps}
          filteredApps={filters.selectedApps}
          onSubmit={(apps) => {
            setFilters((state) => ({ ...state, selectedApps: apps }));
            handlePopUpClose('appList');
          }}
        />
      </Modal>
    </>
  );
}