repositoryRow.tsx 6.8 KB

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