index.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import {Fragment, useMemo, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import EmptyMessage from 'sentry/components/emptyMessage';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import Pagination from 'sentry/components/pagination';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PanelBody from 'sentry/components/panels/panelBody';
  10. import PanelHeader from 'sentry/components/panels/panelHeader';
  11. import PanelItem from 'sentry/components/panels/panelItem';
  12. import Placeholder from 'sentry/components/placeholder';
  13. import SearchBar from 'sentry/components/searchBar';
  14. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  15. import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
  16. import {t} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import type {Project} from 'sentry/types/project';
  19. import {browserHistory} from 'sentry/utils/browserHistory';
  20. import {sortProjects} from 'sentry/utils/project/sortProjects';
  21. import {useApiQuery} from 'sentry/utils/queryClient';
  22. import {decodeScalar} from 'sentry/utils/queryString';
  23. import routeTitleGen from 'sentry/utils/routeTitle';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  27. import ProjectListItem from 'sentry/views/settings/components/settingsProjectItem';
  28. import CreateProjectButton from 'sentry/views/settings/organizationProjects/createProjectButton';
  29. import ProjectStatsGraph from './projectStatsGraph';
  30. const ITEMS_PER_PAGE = 50;
  31. type ProjectStats = Record<string, Required<Project['stats']>>;
  32. function OrganizationProjects() {
  33. const organization = useOrganization();
  34. const location = useLocation();
  35. const query = decodeScalar(location.query.query, '');
  36. const time = useRef(new Date().getTime());
  37. const {
  38. data: projectList,
  39. getResponseHeader,
  40. isPending,
  41. isError,
  42. } = useApiQuery<Project[]>(
  43. [
  44. `/organizations/${organization.slug}/projects/`,
  45. {
  46. query: {
  47. ...location.query,
  48. query,
  49. per_page: ITEMS_PER_PAGE,
  50. },
  51. },
  52. ],
  53. {staleTime: 0}
  54. );
  55. const {data: projectStats, isPending: isLoadingStats} = useApiQuery<ProjectStats>(
  56. [
  57. `/organizations/${organization.slug}/stats/`,
  58. {
  59. query: {
  60. projectID: projectList?.map(p => p.id),
  61. since: time.current / 1000 - 3600 * 24,
  62. stat: 'generated',
  63. group: 'project',
  64. },
  65. },
  66. ],
  67. {
  68. staleTime: 60_000,
  69. enabled: !!projectList,
  70. }
  71. );
  72. const projectListPageLinks = getResponseHeader?.('Link');
  73. const action = <CreateProjectButton />;
  74. const debouncedSearch = useMemo(
  75. () =>
  76. debounce(
  77. (searchQuery: string) =>
  78. browserHistory.replace({
  79. pathname: location.pathname,
  80. query: {...location.query, query: searchQuery, cursor: undefined},
  81. }),
  82. DEFAULT_DEBOUNCE_DURATION
  83. ),
  84. [location.pathname, location.query]
  85. );
  86. return (
  87. <Fragment>
  88. <SentryDocumentTitle
  89. title={routeTitleGen(t('Projects'), organization.slug, false)}
  90. />
  91. <SettingsPageHeader title="Projects" action={action} />
  92. <SearchWrapper>
  93. <SearchBar
  94. placeholder={t('Search Projects')}
  95. onChange={debouncedSearch}
  96. query={query}
  97. />
  98. </SearchWrapper>
  99. <Panel>
  100. <PanelHeader>{t('Projects')}</PanelHeader>
  101. <PanelBody>
  102. {isPending && <LoadingIndicator />}
  103. {isError && <LoadingError />}
  104. {projectList &&
  105. sortProjects(projectList).map(project => (
  106. <GridPanelItem key={project.id}>
  107. <ProjectListItemWrapper>
  108. <ProjectListItem project={project} organization={organization} />
  109. </ProjectListItemWrapper>
  110. <ProjectStatsGraphWrapper>
  111. {isLoadingStats && <Placeholder height="25px" />}
  112. {projectStats && (
  113. <ProjectStatsGraph
  114. key={project.id}
  115. project={project}
  116. stats={projectStats[project.id]}
  117. />
  118. )}
  119. </ProjectStatsGraphWrapper>
  120. </GridPanelItem>
  121. ))}
  122. {projectList && projectList.length === 0 && (
  123. <EmptyMessage>{t('No projects found.')}</EmptyMessage>
  124. )}
  125. </PanelBody>
  126. </Panel>
  127. {projectListPageLinks && <Pagination pageLinks={projectListPageLinks} />}
  128. </Fragment>
  129. );
  130. }
  131. export default OrganizationProjects;
  132. const SearchWrapper = styled('div')`
  133. margin-bottom: ${space(2)};
  134. `;
  135. const GridPanelItem = styled(PanelItem)`
  136. display: flex;
  137. align-items: center;
  138. padding: 0;
  139. `;
  140. const ProjectListItemWrapper = styled('div')`
  141. padding: ${space(2)};
  142. flex: 1;
  143. `;
  144. const ProjectStatsGraphWrapper = styled('div')`
  145. padding: ${space(2)};
  146. width: 25%;
  147. margin-left: ${space(2)};
  148. `;