codeOwnerFileTable.tsx 5.4 KB

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