codeOwnerFileTable.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  6. import ExternalLink from 'sentry/components/links/externalLink';
  7. import {PanelTable} from 'sentry/components/panels/panelTable';
  8. import TimeSince from 'sentry/components/timeSince';
  9. import {IconEllipsis, IconOpen} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {CodeOwner, CodeownersFile} from 'sentry/types/integrations';
  13. import type {Project} from 'sentry/types/project';
  14. import {getCodeOwnerIcon} from 'sentry/utils/integrationUtil';
  15. import useApi from 'sentry/utils/useApi';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import ViewCodeOwnerModal, {modalCss} from './viewCodeOwnerModal';
  18. interface CodeOwnerFileTableProps {
  19. codeowners: CodeOwner[];
  20. disabled: boolean;
  21. onDelete: (data: CodeOwner) => void;
  22. onUpdate: (data: CodeOwner) => void;
  23. project: Project;
  24. }
  25. /**
  26. * A list of codeowner files being used for this project
  27. * If you're looking for ownership rules table see `OwnershipRulesTable`
  28. */
  29. export function CodeOwnerFileTable({
  30. codeowners,
  31. project,
  32. onUpdate,
  33. onDelete,
  34. disabled,
  35. }: CodeOwnerFileTableProps) {
  36. const api = useApi();
  37. const organization = useOrganization();
  38. // Do we need an empty state instead?
  39. if (codeowners.length === 0) {
  40. return null;
  41. }
  42. const handleView = (codeowner: CodeOwner) => () => {
  43. // Open modal with codeowner file
  44. openModal(deps => <ViewCodeOwnerModal {...deps} codeowner={codeowner} />, {modalCss});
  45. };
  46. const handleSync = (codeowner: CodeOwner) => async () => {
  47. try {
  48. const codeownerFile: CodeownersFile = await api.requestPromise(
  49. `/organizations/${organization.slug}/code-mappings/${codeowner.codeMappingId}/codeowners/`,
  50. {
  51. method: 'GET',
  52. }
  53. );
  54. const data = await api.requestPromise(
  55. `/projects/${organization.slug}/${project.slug}/codeowners/${codeowner.id}/`,
  56. {
  57. method: 'PUT',
  58. data: {raw: codeownerFile.raw, date_updated: new Date().toISOString()},
  59. }
  60. );
  61. onUpdate({...codeowner, ...data});
  62. addSuccessMessage(t('CODEOWNERS file sync successful.'));
  63. } catch (_err) {
  64. addErrorMessage(t('An error occurred trying to sync CODEOWNERS file.'));
  65. }
  66. };
  67. const handleDelete = (codeowner: CodeOwner) => async () => {
  68. try {
  69. await api.requestPromise(
  70. `/projects/${organization.slug}/${project.slug}/codeowners/${codeowner.id}/`,
  71. {
  72. method: 'DELETE',
  73. }
  74. );
  75. onDelete(codeowner);
  76. addSuccessMessage(t('Deletion successful'));
  77. } catch {
  78. // no 4xx errors should happen on delete
  79. addErrorMessage(t('An error occurred'));
  80. }
  81. };
  82. return (
  83. <StyledPanelTable
  84. headers={[
  85. t('codeowners'),
  86. t('Stack Trace Root'),
  87. t('Source Code Root'),
  88. t('Last Synced'),
  89. t('File'),
  90. '',
  91. ]}
  92. >
  93. {codeowners.map(codeowner => (
  94. <Fragment key={codeowner.id}>
  95. <FlexCenter>
  96. {getCodeOwnerIcon(codeowner.provider)}
  97. {codeowner.codeMapping?.repoName}
  98. </FlexCenter>
  99. <FlexCenter>
  100. <code>{codeowner.codeMapping?.stackRoot}</code>
  101. </FlexCenter>
  102. <FlexCenter>
  103. <code>{codeowner.codeMapping?.sourceRoot}</code>
  104. </FlexCenter>
  105. <FlexCenter>
  106. <TimeSince date={codeowner.dateUpdated} />
  107. </FlexCenter>
  108. <FlexCenter>
  109. {codeowner.codeOwnersUrl === 'unknown' ? null : (
  110. <StyledExternalLink href={codeowner.codeOwnersUrl}>
  111. <IconOpen size="xs" />
  112. {t(
  113. 'View in %s',
  114. codeowner.codeMapping?.provider?.name ?? codeowner.provider
  115. )}
  116. </StyledExternalLink>
  117. )}
  118. </FlexCenter>
  119. <FlexCenter>
  120. <DropdownMenu
  121. items={[
  122. {
  123. key: 'view',
  124. label: t('View'),
  125. onAction: handleView(codeowner),
  126. },
  127. {
  128. key: 'sync',
  129. label: t('Sync'),
  130. onAction: handleSync(codeowner),
  131. },
  132. {
  133. key: 'delete',
  134. label: t('Delete'),
  135. priority: 'danger',
  136. onAction: handleDelete(codeowner),
  137. },
  138. ]}
  139. position="bottom-end"
  140. triggerProps={{
  141. 'aria-label': t('Actions'),
  142. size: 'xs',
  143. icon: <IconEllipsis />,
  144. showChevron: false,
  145. disabled,
  146. }}
  147. disabledKeys={disabled ? ['sync', 'delete'] : []}
  148. />
  149. </FlexCenter>
  150. </Fragment>
  151. ))}
  152. </StyledPanelTable>
  153. );
  154. }
  155. const StyledPanelTable = styled(PanelTable)`
  156. grid-template-columns: 1fr 1fr 1fr auto min-content min-content;
  157. position: static;
  158. overflow: auto;
  159. white-space: nowrap;
  160. `;
  161. const FlexCenter = styled('div')`
  162. display: flex;
  163. align-items: center;
  164. gap: ${space(1)};
  165. `;
  166. const StyledExternalLink = styled(ExternalLink)`
  167. display: flex;
  168. align-items: center;
  169. gap: ${space(1)};
  170. `;