index.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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. since: time.current / 1000 - 3600 * 24,
  61. stat: 'generated',
  62. group: 'project',
  63. per_page: ITEMS_PER_PAGE,
  64. },
  65. },
  66. ],
  67. {staleTime: 0}
  68. );
  69. const projectListPageLinks = getResponseHeader?.('Link');
  70. const action = <CreateProjectButton />;
  71. const debouncedSearch = useMemo(
  72. () =>
  73. debounce(
  74. (searchQuery: string) =>
  75. browserHistory.replace({
  76. pathname: location.pathname,
  77. query: {...location.query, query: searchQuery},
  78. }),
  79. DEFAULT_DEBOUNCE_DURATION
  80. ),
  81. [location.pathname, location.query]
  82. );
  83. return (
  84. <Fragment>
  85. <SentryDocumentTitle
  86. title={routeTitleGen(t('Projects'), organization.slug, false)}
  87. />
  88. <SettingsPageHeader title="Projects" action={action} />
  89. <SearchWrapper>
  90. <SearchBar
  91. placeholder={t('Search Projects')}
  92. onChange={debouncedSearch}
  93. query={query}
  94. />
  95. </SearchWrapper>
  96. <Panel>
  97. <PanelHeader>{t('Projects')}</PanelHeader>
  98. <PanelBody>
  99. {isPending && <LoadingIndicator />}
  100. {isError && <LoadingError />}
  101. {projectList &&
  102. sortProjects(projectList).map(project => (
  103. <GridPanelItem key={project.id}>
  104. <ProjectListItemWrapper>
  105. <ProjectListItem project={project} organization={organization} />
  106. </ProjectListItemWrapper>
  107. <ProjectStatsGraphWrapper>
  108. {isLoadingStats && <Placeholder height="25px" />}
  109. {projectStats && (
  110. <ProjectStatsGraph
  111. key={project.id}
  112. project={project}
  113. stats={projectStats[project.id]}
  114. />
  115. )}
  116. </ProjectStatsGraphWrapper>
  117. </GridPanelItem>
  118. ))}
  119. {projectList && projectList.length === 0 && (
  120. <EmptyMessage>{t('No projects found.')}</EmptyMessage>
  121. )}
  122. </PanelBody>
  123. </Panel>
  124. {projectListPageLinks && <Pagination pageLinks={projectListPageLinks} />}
  125. </Fragment>
  126. );
  127. }
  128. export default OrganizationProjects;
  129. const SearchWrapper = styled('div')`
  130. margin-bottom: ${space(2)};
  131. `;
  132. const GridPanelItem = styled(PanelItem)`
  133. display: flex;
  134. align-items: center;
  135. padding: 0;
  136. `;
  137. const ProjectListItemWrapper = styled('div')`
  138. padding: ${space(2)};
  139. flex: 1;
  140. `;
  141. const ProjectStatsGraphWrapper = styled('div')`
  142. padding: ${space(2)};
  143. width: 25%;
  144. margin-left: ${space(2)};
  145. `;