123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {openModal} from 'sentry/actionCreators/modal';
- import {DropdownMenu} from 'sentry/components/dropdownMenu';
- import ExternalLink from 'sentry/components/links/externalLink';
- import PanelTable from 'sentry/components/panels/panelTable';
- import TimeSince from 'sentry/components/timeSince';
- import {IconEllipsis, IconOpen} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {CodeOwner, CodeownersFile, Project} from 'sentry/types';
- import {getCodeOwnerIcon} from 'sentry/utils/integrationUtil';
- import useApi from 'sentry/utils/useApi';
- import useOrganization from 'sentry/utils/useOrganization';
- import ViewCodeOwnerModal, {modalCss} from './viewCodeOwnerModal';
- interface CodeOwnerFileTableProps {
- codeowners: CodeOwner[];
- disabled: boolean;
- onDelete: (data: CodeOwner) => void;
- onUpdate: (data: CodeOwner) => void;
- project: Project;
- }
- /**
- * A list of codeowner files being used for this project
- * If you're looking for ownership rules table see `OwnershipRulesTable`
- */
- export function CodeOwnerFileTable({
- codeowners,
- project,
- onUpdate,
- onDelete,
- disabled,
- }: CodeOwnerFileTableProps) {
- const api = useApi();
- const organization = useOrganization();
- // Do we need an empty state instead?
- if (codeowners.length === 0) {
- return null;
- }
- const handleView = (codeowner: CodeOwner) => () => {
- // Open modal with codeowner file
- openModal(deps => <ViewCodeOwnerModal {...deps} codeowner={codeowner} />, {modalCss});
- };
- const handleSync = (codeowner: CodeOwner) => async () => {
- try {
- const codeownerFile: CodeownersFile = await api.requestPromise(
- `/organizations/${organization.slug}/code-mappings/${codeowner.codeMappingId}/codeowners/`,
- {
- method: 'GET',
- }
- );
- const data = await api.requestPromise(
- `/projects/${organization.slug}/${project.slug}/codeowners/${codeowner.id}/`,
- {
- method: 'PUT',
- data: {raw: codeownerFile.raw},
- }
- );
- onUpdate({...codeowner, ...data});
- addSuccessMessage(t('CODEOWNERS file sync successful.'));
- } catch (_err) {
- addErrorMessage(t('An error occurred trying to sync CODEOWNERS file.'));
- }
- };
- const handleDelete = (codeowner: CodeOwner) => async () => {
- try {
- await api.requestPromise(
- `/projects/${organization.slug}/${project.slug}/codeowners/${codeowner.id}/`,
- {
- method: 'DELETE',
- }
- );
- onDelete(codeowner);
- addSuccessMessage(t('Deletion successful'));
- } catch {
- // no 4xx errors should happen on delete
- addErrorMessage(t('An error occurred'));
- }
- };
- return (
- <StyledPanelTable
- headers={[
- t('codeowners'),
- t('Stack Trace Root'),
- t('Source Code Root'),
- t('Last Synced'),
- t('File'),
- '',
- ]}
- >
- {codeowners.map(codeowner => (
- <Fragment key={codeowner.id}>
- <FlexCenter>
- {getCodeOwnerIcon(codeowner.provider)}
- {codeowner.codeMapping?.repoName}
- </FlexCenter>
- <FlexCenter>
- <code>{codeowner.codeMapping?.stackRoot}</code>
- </FlexCenter>
- <FlexCenter>
- <code>{codeowner.codeMapping?.sourceRoot}</code>
- </FlexCenter>
- <FlexCenter>
- <TimeSince date={codeowner.dateUpdated} />
- </FlexCenter>
- <FlexCenter>
- {codeowner.codeOwnersUrl === 'unknown' ? null : (
- <StyledExternalLink href={codeowner.codeOwnersUrl}>
- <IconOpen size="xs" />
- {t(
- 'View in %s',
- codeowner.codeMapping?.provider?.name ?? codeowner.provider
- )}
- </StyledExternalLink>
- )}
- </FlexCenter>
- <FlexCenter>
- <DropdownMenu
- items={[
- {
- key: 'view',
- label: t('View'),
- onAction: handleView(codeowner),
- },
- {
- key: 'sync',
- label: t('Sync'),
- onAction: handleSync(codeowner),
- },
- {
- key: 'delete',
- label: t('Delete'),
- priority: 'danger',
- onAction: handleDelete(codeowner),
- },
- ]}
- position="bottom-end"
- triggerProps={{
- 'aria-label': t('Actions'),
- size: 'xs',
- icon: <IconEllipsis size="xs" />,
- showChevron: false,
- disabled,
- }}
- disabledKeys={disabled ? ['sync', 'delete'] : []}
- />
- </FlexCenter>
- </Fragment>
- ))}
- </StyledPanelTable>
- );
- }
- const StyledPanelTable = styled(PanelTable)`
- grid-template-columns: 1fr 1fr 1fr auto min-content min-content;
- position: static;
- overflow: auto;
- white-space: nowrap;
- `;
- const FlexCenter = styled('div')`
- display: flex;
- align-items: center;
- gap: ${space(1)};
- `;
- const StyledExternalLink = styled(ExternalLink)`
- display: flex;
- align-items: center;
- gap: ${space(1)};
- `;
|