hooks#useDebounce TypeScript Examples

The following examples show how to use hooks#useDebounce. 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: Input.tsx    From frontend with GNU General Public License v3.0 5 votes vote down vote up
Input = forwardRef<HTMLInputElement, IProps>(
  (
    {
      type = 'text',
      icon: Icon,
      label,
      placeholder,
      onChange,
      fullWidth,
      error,
      name,
      debounce,
      id,
      ...props
    }: IProps,
    ref
  ): JSX.Element => {
    const debounceEffect = useDebounce();
    const handleOnChange = (action: () => void) => {
      if (!action) {
        return;
      }
      debounce ? debounceEffect(action, debounce) : action();
    };
    const inputLabelId = label && id && `${id}-label`;

    return (
      <Container fullWidth={fullWidth} {...props}>
        <InputWrapper fullWidth={fullWidth}>
          {label && <Label htmlFor={inputLabelId}>{label}</Label>}
          <StyledInput
            id={inputLabelId}
            isIcon={!!Icon}
            type={type}
            placeholder={placeholder}
            onChange={() => handleOnChange(onChange)}
            name={name}
            ref={ref}
            {...props}
          />
          {Icon && (
            <StyledIcon data-testid="icon">
              <Icon />
            </StyledIcon>
          )}
        </InputWrapper>
        {error && console.log(error)}
      </Container>
    );
  }
)
Example #2
Source File: ExploreApiSearch.tsx    From substrate-api-explorer with Apache License 2.0 5 votes vote down vote up
ExploreApiSearch = ({ focusOnMount, query, storybookDemo }: Props) => {
  const historyHook = useHistory()
  const history = storybookDemo ? {} : historyHook

  const [searchQuery, setSearchQuery] = useState<string>(
    query ? decodeURIComponent(query) : ''
  )

  const handleRedirectBack = () => {
    if (storybookDemo) {
      setSearchQuery('')
    } else {
      history.push({
        pathname: '/explore-api',
        state: { routeName: 'Explore API', fromSearch: true }
      })
    }
  }

  const handleSearch = () => {
    if (!storybookDemo) {
      if (decodeURIComponent(searchQuery).trim()) {
        history.push({
          pathname: `/search/${encodeURIComponent(searchQuery.trim())}`,
          state: { routeName: 'Search' }
        })
      } else {
        handleRedirectBack()
      }
    }
  }

  useDebounce(400, handleSearch, [searchQuery])

  return (
    <S.Wrapper
      focusOnMount={focusOnMount}
      placeholder="Search API..."
      value={searchQuery}
      onChange={e => setSearchQuery(e.target.value)}
      onReset={handleRedirectBack}
    />
  )
}
Example #3
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 #4
Source File: HomePage.tsx    From one-platform with MIT License 4 votes vote down vote up
HomePage = (): JSX.Element => {
  const [search, setSearch] = useState('');
  const navigate = useNavigate();
  const debouncedSearch = useDebounce(search);
  const { isLoading, data: namespace } = useGetNamespaceList({ search: debouncedSearch });

  const onSearchChange = (value: string): void => {
    setSearch(value);
  };

  const onSearch = () => {
    navigate(`/apis?search=${search}`);
  };

  // memorized function to render the async search results
  const searchResult = useMemo(() => {
    const results = namespace?.listNamespaces?.data;

    if (isLoading) {
      return (
        <MenuItem key="search-menu">
          Searching <Spinner size="sm" />
        </MenuItem>
      );
    }

    if (!results?.length) {
      return [
        <MenuItem key="search-menu-no-result" isDisabled>
          No results were found
        </MenuItem>,
        <MenuItem onClick={onSearch} itemId="loader" key="search-menu-view-more">
          View more
        </MenuItem>,
      ];
    }

    return results?.map(({ name, id, schemas, slug }, index) => [
      <Link to={`/apis/${slug}`} className="catalog-nav-link" key={`${id}-${index + 1}`}>
        <MenuItem
          description={
            <Split className="pf-u-mt-xs">
              <SplitItem className="pf-u-mr-xs">
                <Text component={TextVariants.small} className="pf-u-color-200">
                  Schema(s):
                </Text>
              </SplitItem>
              {schemas.map(({ name: sName, id: sId, category }) => (
                <SplitItem key={sId} className="pf-u-mr-xs">
                  <Label
                    isCompact
                    color="blue"
                    icon={
                      <img
                        src={category === ApiCategory.GRAPHQL ? GRAPHQL_LOGO : REST_LOGO}
                        width="12px"
                        alt="graphql rest"
                        className="pf-u-mt-xs"
                      />
                    }
                  >
                    <Text component={TextVariants.small} className="pf-u-color-200">
                      {sName}
                    </Text>
                  </Label>
                </SplitItem>
              ))}
            </Split>
          }
        >
          {name}
        </MenuItem>
      </Link>,
      index !== (results?.length || 0) - 1 && <Divider key={`${id}-${index + 2}`} />,
      index === (results?.length || 1) - 1 && (
        <MenuItem onClick={onSearch} itemId="loader" key="search-menu-view-more">
          View more
        </MenuItem>
      ),
    ]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [namespace?.listNamespaces, isLoading]);

  return (
    <>
      <PageSection className={css(styles.header, 'pf-m-align-center')} isWidthLimited>
        <Stack className="pf-u-p-xl pf-u-pt-3xl" hasGutter>
          <StackItem>
            <Split>
              <SplitItem isFilled>
                <Stack style={{ maxWidth: '620px' }}>
                  <StackItem className="pf-u-mb-md">
                    <Title headingLevel="h1" size="4xl">
                      API Catalog
                    </Title>
                  </StackItem>
                  <StackItem>
                    <Text className="pf-u-font-size-xl">
                      A catalog of APIs to manage, promote and share APIs with developers and users.
                    </Text>
                  </StackItem>
                  <StackItem>
                    <Link to={ApiCatalogLinks.ListPage}>
                      <Button variant="primary" isBlock className={styles['header-explore-button']}>
                        <Split className="pf-u-align-items-center">
                          <SplitItem>Discover a wide list of APIs</SplitItem>
                          <SplitItem isFilled />
                          <SplitItem>Explore</SplitItem>
                          <SplitItem className="pf-u-ml-sm">
                            <ArrowRightIcon size="sm" />
                          </SplitItem>
                        </Split>
                      </Button>
                    </Link>
                  </StackItem>
                </Stack>
              </SplitItem>
              <SplitItem>
                <Bullseye>
                  <img src={OP_CONTAINER_LOGO} alt="api catalog" width="160cm" />
                </Bullseye>
              </SplitItem>
            </Split>
          </StackItem>
          <StackItem className="pf-u-mt-2xl">
            <Bullseye className={styles['catalog-search-container']}>
              <div className="pf-u-w-75">
                <SearchBar placeholder="Search for APIs" value={search} onChange={onSearchChange} />
              </div>
              {search && debouncedSearch && (
                <Menu className={styles['catalog-search-menu']}>
                  <MenuList>{searchResult}</MenuList>
                </Menu>
              )}
            </Bullseye>
          </StackItem>
        </Stack>
      </PageSection>
      <PageSection
        isWidthLimited
        className={css('pf-m-align-center', styles['add-api-section--container'])}
        variant="dark"
      >
        <Bullseye className="pf-u-px-xl">
          <Split className={css(styles['add-api-section'], 'pf-u-w-100 pf-u-px-4xl pf-u-py-xl')}>
            <SplitItem isFilled>
              <Text component={TextVariants.small}>ADD DATASOURCE</Text>
              <Text className="pf-u-font-size-2xl">Want to add an API to the Catalog?</Text>
            </SplitItem>
            <SplitItem className="pf-l-flex pf-m-align-items-center">
              <Link to={ApiCatalogLinks.AddNewApiPage}>
                <Button variant="secondary">Add API</Button>
              </Link>
            </SplitItem>
          </Split>
        </Bullseye>
      </PageSection>
    </>
  );
}
Example #5
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>
    </>
  );
}