repositoryRow.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. cancelDeleteRepository,
  5. deleteRepository,
  6. } from 'sentry/actionCreators/integrations';
  7. import {openModal} from 'sentry/actionCreators/modal';
  8. import {Client} from 'sentry/api';
  9. import Access from 'sentry/components/acl/access';
  10. import Button from 'sentry/components/button';
  11. import Confirm from 'sentry/components/confirm';
  12. import ExternalLink from 'sentry/components/links/externalLink';
  13. import {PanelItem} from 'sentry/components/panels';
  14. import RepositoryEditForm from 'sentry/components/repositoryEditForm';
  15. import Tooltip from 'sentry/components/tooltip';
  16. import {IconDelete, IconEdit} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import space from 'sentry/styles/space';
  19. import {Organization, Repository, RepositoryStatus} from 'sentry/types';
  20. import withOrganization from 'sentry/utils/withOrganization';
  21. type Props = {
  22. api: Client;
  23. orgId: string;
  24. organization: Organization;
  25. repository: Repository;
  26. onRepositoryChange?: (data: {id: string; status: RepositoryStatus}) => void;
  27. showProvider?: boolean;
  28. };
  29. function getStatusLabel(repo: Repository) {
  30. switch (repo.status) {
  31. case RepositoryStatus.PENDING_DELETION:
  32. return 'Deletion Queued';
  33. case RepositoryStatus.DELETION_IN_PROGRESS:
  34. return 'Deletion in Progress';
  35. case RepositoryStatus.DISABLED:
  36. return 'Disabled';
  37. case RepositoryStatus.HIDDEN:
  38. return 'Disabled';
  39. default:
  40. return null;
  41. }
  42. }
  43. function RepositoryRow({
  44. api,
  45. repository,
  46. onRepositoryChange,
  47. organization,
  48. orgId,
  49. showProvider = false,
  50. }: Props) {
  51. const isCustomRepo =
  52. organization.features.includes('integrations-custom-scm') &&
  53. repository.provider.id === 'integrations:custom_scm';
  54. const isActive = repository.status === RepositoryStatus.ACTIVE;
  55. const cancelDelete = () =>
  56. cancelDeleteRepository(api, orgId, repository.id).then(
  57. data => {
  58. if (onRepositoryChange) {
  59. onRepositoryChange(data);
  60. }
  61. },
  62. () => {}
  63. );
  64. const deleteRepo = () =>
  65. deleteRepository(api, orgId, repository.id).then(
  66. data => {
  67. if (onRepositoryChange) {
  68. onRepositoryChange(data);
  69. }
  70. },
  71. () => {}
  72. );
  73. const handleEditRepo = (data: Repository) => {
  74. onRepositoryChange?.(data);
  75. };
  76. const renderDeleteButton = (hasAccess: boolean) => (
  77. <Tooltip
  78. title={t(
  79. 'You must be an organization owner, manager or admin to remove a repository.'
  80. )}
  81. disabled={hasAccess}
  82. >
  83. <Confirm
  84. disabled={
  85. !hasAccess || (!isActive && repository.status !== RepositoryStatus.DISABLED)
  86. }
  87. onConfirm={deleteRepo}
  88. message={t(
  89. 'Are you sure you want to remove this repository? All associated commit data will be removed in addition to the repository.'
  90. )}
  91. >
  92. <StyledButton
  93. size="xs"
  94. icon={<IconDelete size="xs" />}
  95. aria-label={t('delete')}
  96. disabled={!hasAccess}
  97. />
  98. </Confirm>
  99. </Tooltip>
  100. );
  101. const triggerModal = () =>
  102. openModal(({Body, Header, closeModal}) => (
  103. <Fragment>
  104. <Header closeButton>{t('Edit Repository')}</Header>
  105. <Body>
  106. <RepositoryEditForm
  107. orgSlug={orgId}
  108. repository={repository}
  109. onSubmitSuccess={handleEditRepo}
  110. closeModal={closeModal}
  111. onCancel={closeModal}
  112. />
  113. </Body>
  114. </Fragment>
  115. ));
  116. return (
  117. <Access access={['org:integrations']}>
  118. {({hasAccess}) => (
  119. <StyledPanelItem status={repository.status}>
  120. <RepositoryTitleAndUrl>
  121. <RepositoryTitle>
  122. <strong>{repository.name}</strong>
  123. {!isActive && <small> &mdash; {getStatusLabel(repository)}</small>}
  124. {repository.status === RepositoryStatus.PENDING_DELETION && (
  125. <StyledButton
  126. size="xs"
  127. onClick={cancelDelete}
  128. disabled={!hasAccess}
  129. data-test-id="repo-cancel"
  130. >
  131. {t('Cancel')}
  132. </StyledButton>
  133. )}
  134. </RepositoryTitle>
  135. <div>
  136. {showProvider && <small>{repository.provider.name}</small>}
  137. {showProvider && repository.url && <span>&nbsp;&mdash;&nbsp;</span>}
  138. {repository.url && (
  139. <small>
  140. <ExternalLink href={repository.url}>
  141. {repository.url.replace('https://', '')}
  142. </ExternalLink>
  143. </small>
  144. )}
  145. </div>
  146. </RepositoryTitleAndUrl>
  147. {isCustomRepo ? (
  148. <EditAndDelete>
  149. <StyledButton
  150. size="xs"
  151. icon={<IconEdit size="xs" />}
  152. aria-label={t('edit')}
  153. disabled={
  154. !hasAccess ||
  155. (!isActive && repository.status !== RepositoryStatus.DISABLED)
  156. }
  157. onClick={triggerModal}
  158. />
  159. {renderDeleteButton(hasAccess)}
  160. </EditAndDelete>
  161. ) : (
  162. renderDeleteButton(hasAccess)
  163. )}
  164. </StyledPanelItem>
  165. )}
  166. </Access>
  167. );
  168. }
  169. const StyledPanelItem = styled(PanelItem)<{status: RepositoryStatus}>`
  170. /* shorter top padding because of title lineheight */
  171. padding: ${space(1)} ${space(2)} ${space(2)};
  172. justify-content: space-between;
  173. align-items: center;
  174. flex: 1;
  175. ${p =>
  176. p.status === RepositoryStatus.DISABLED &&
  177. `
  178. filter: grayscale(1);
  179. opacity: 0.4;
  180. `};
  181. &:last-child {
  182. border-bottom: none;
  183. }
  184. `;
  185. const StyledButton = styled(Button)`
  186. margin-left: ${space(1)};
  187. `;
  188. const RepositoryTitleAndUrl = styled('div')`
  189. display: flex;
  190. flex-direction: column;
  191. `;
  192. const EditAndDelete = styled('div')`
  193. display: flex;
  194. margin-left: ${space(1)};
  195. `;
  196. const RepositoryTitle = styled('div')`
  197. /* accommodate cancel button height */
  198. line-height: 26px;
  199. `;
  200. export default withOrganization(RepositoryRow);