index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import {useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {openDebugFileSourceModal} from 'sentry/actionCreators/modal';
  6. import type {Client} from 'sentry/api';
  7. import Access from 'sentry/components/acl/access';
  8. import Feature from 'sentry/components/acl/feature';
  9. import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
  10. import DropdownButton from 'sentry/components/dropdownButton';
  11. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  12. import MenuItem from 'sentry/components/menuItem';
  13. import Panel from 'sentry/components/panels/panel';
  14. import PanelBody from 'sentry/components/panels/panelBody';
  15. import PanelHeader from 'sentry/components/panels/panelHeader';
  16. import {Tooltip} from 'sentry/components/tooltip';
  17. import {t} from 'sentry/locale';
  18. import ProjectsStore from 'sentry/stores/projectsStore';
  19. import type {CustomRepo, CustomRepoType} from 'sentry/types/debugFiles';
  20. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  21. import type {Organization} from 'sentry/types/organization';
  22. import type {Project} from 'sentry/types/project';
  23. import {defined} from 'sentry/utils';
  24. import Repository from './repository';
  25. import {dropDownItems, expandKeys, getRequestMessages} from './utils';
  26. type Props = {
  27. api: Client;
  28. customRepositories: CustomRepo[];
  29. location: Location;
  30. organization: Organization;
  31. project: Project;
  32. router: InjectedRouter;
  33. };
  34. function CustomRepositories({
  35. api,
  36. organization,
  37. customRepositories: repositories,
  38. project,
  39. router,
  40. location,
  41. }: Props) {
  42. const orgSlug = organization.slug;
  43. const persistData = useCallback(
  44. ({
  45. updatedItems,
  46. updatedItem,
  47. index,
  48. refresh,
  49. }: {
  50. index?: number;
  51. refresh?: boolean;
  52. updatedItem?: CustomRepo;
  53. updatedItems?: CustomRepo[];
  54. }) => {
  55. let items = updatedItems ?? [];
  56. if (updatedItem && defined(index)) {
  57. items = [...repositories];
  58. items.splice(index, 1, updatedItem);
  59. }
  60. const {successMessage, errorMessage} = getRequestMessages(
  61. items.length,
  62. repositories.length
  63. );
  64. const symbolSources = JSON.stringify(items.map(expandKeys));
  65. const promise: Promise<any> = api.requestPromise(
  66. `/projects/${orgSlug}/${project.slug}/`,
  67. {
  68. method: 'PUT',
  69. data: {symbolSources},
  70. }
  71. );
  72. promise.catch(() => {
  73. addErrorMessage(errorMessage);
  74. });
  75. promise.then(result => {
  76. ProjectsStore.onUpdateSuccess(result);
  77. addSuccessMessage(successMessage);
  78. if (refresh) {
  79. window.location.reload();
  80. }
  81. });
  82. return promise;
  83. },
  84. [api, orgSlug, project.slug, repositories]
  85. );
  86. const handleCloseModal = useCallback(() => {
  87. router.push({
  88. ...location,
  89. query: {
  90. ...location.query,
  91. customRepository: undefined,
  92. },
  93. });
  94. }, [location, router]);
  95. const openDebugFileSourceDialog = useCallback(() => {
  96. const {customRepository} = location.query;
  97. if (!customRepository) {
  98. return;
  99. }
  100. const itemIndex = repositories.findIndex(
  101. repository => repository.id === customRepository
  102. );
  103. const item = repositories[itemIndex];
  104. if (!item) {
  105. return;
  106. }
  107. openDebugFileSourceModal({
  108. organization,
  109. sourceConfig: item,
  110. sourceType: item.type,
  111. onSave: updatedItem =>
  112. persistData({updatedItem: updatedItem as CustomRepo, index: itemIndex}),
  113. onClose: handleCloseModal,
  114. });
  115. }, [handleCloseModal, location.query, organization, persistData, repositories]);
  116. useEffect(() => {
  117. openDebugFileSourceDialog();
  118. }, [location.query, openDebugFileSourceDialog]);
  119. function handleAddRepository(repoType: CustomRepoType) {
  120. openDebugFileSourceModal({
  121. organization,
  122. sourceType: repoType,
  123. onSave: updatedData =>
  124. persistData({updatedItems: [...repositories, updatedData] as CustomRepo[]}),
  125. });
  126. }
  127. function handleDeleteRepository(repoId: CustomRepo['id']) {
  128. const newRepositories = [...repositories];
  129. const index = newRepositories.findIndex(item => item.id === repoId);
  130. newRepositories.splice(index, 1);
  131. persistData({
  132. updatedItems: newRepositories as CustomRepo[],
  133. refresh: false,
  134. });
  135. }
  136. function handleEditRepository(repoId: CustomRepo['id']) {
  137. router.push({
  138. ...location,
  139. query: {
  140. ...location.query,
  141. customRepository: repoId,
  142. },
  143. });
  144. }
  145. return (
  146. <Feature features="custom-symbol-sources" organization={organization}>
  147. {({hasFeature}) => (
  148. <Access access={['project:write']} project={project}>
  149. {({hasAccess}) => {
  150. const addRepositoryButtonDisabled = !hasAccess;
  151. return (
  152. <Panel>
  153. <PanelHeader hasButtons>
  154. {t('Custom Repositories')}
  155. <Tooltip
  156. title={
  157. !hasAccess
  158. ? t('You do not have permission to add custom repositories.')
  159. : undefined
  160. }
  161. >
  162. <DropdownAutoComplete
  163. alignMenu="right"
  164. disabled={addRepositoryButtonDisabled}
  165. onSelect={item => handleAddRepository(item.value)}
  166. items={dropDownItems.map(dropDownItem => ({
  167. ...dropDownItem,
  168. label: (
  169. <DropDownLabel
  170. aria-label={t(
  171. 'Open %s custom repository modal',
  172. dropDownItem.label
  173. )}
  174. >
  175. {dropDownItem.label}
  176. </DropDownLabel>
  177. ),
  178. }))}
  179. >
  180. {({isOpen}) => (
  181. <DropdownButton
  182. isOpen={isOpen}
  183. disabled={addRepositoryButtonDisabled}
  184. size="xs"
  185. aria-label={t('Add Repository')}
  186. >
  187. {t('Add Repository')}
  188. </DropdownButton>
  189. )}
  190. </DropdownAutoComplete>
  191. </Tooltip>
  192. </PanelHeader>
  193. <PanelBody>
  194. {!repositories.length ? (
  195. <EmptyStateWarning>
  196. <p>{t('No custom repositories configured')}</p>
  197. </EmptyStateWarning>
  198. ) : (
  199. repositories.map(repository => (
  200. <Repository
  201. key={repository.id}
  202. repository={repository}
  203. hasFeature={hasFeature}
  204. hasAccess={hasAccess}
  205. onDelete={handleDeleteRepository}
  206. onEdit={handleEditRepository}
  207. />
  208. ))
  209. )}
  210. </PanelBody>
  211. </Panel>
  212. );
  213. }}
  214. </Access>
  215. )}
  216. </Feature>
  217. );
  218. }
  219. export default CustomRepositories;
  220. const DropDownLabel = styled(MenuItem)`
  221. color: ${p => p.theme.textColor};
  222. font-size: ${p => p.theme.fontSizeMedium};
  223. font-weight: ${p => p.theme.fontWeightNormal};
  224. text-transform: none;
  225. span {
  226. padding: 0;
  227. }
  228. `;