diff --git a/static/app/views/settings/project/projectOwnership/ownershipRulesTable.spec.tsx b/static/app/views/settings/project/projectOwnership/ownershipRulesTable.spec.tsx index 0f3977ad274fc1..584134e665e34c 100644 --- a/static/app/views/settings/project/projectOwnership/ownershipRulesTable.spec.tsx +++ b/static/app/views/settings/project/projectOwnership/ownershipRulesTable.spec.tsx @@ -212,4 +212,90 @@ describe('OwnershipRulesTable', () => { expect(screen.getByText('mytag30')).toBeInTheDocument(); expect(screen.queryByText('mytag1')).not.toBeInTheDocument(); }); + + it('should render codeowner exclusion rules with no owners', async () => { + const codeownerRules: ParsedOwnershipRule[] = [ + { + matcher: {pattern: '/apps/github', type: 'codeowners'}, + owners: [], + }, + { + matcher: {pattern: 'src/utils/*', type: 'codeowners'}, + owners: [{type: 'user', id: user1.id, name: user1.name}], + }, + ]; + + render( + + ); + + // Clear the "My Teams" filter to see all rules + await userEvent.click(screen.getByRole('button', {name: 'My Teams'})); + await userEvent.click(screen.getByRole('button', {name: 'Clear'})); + + expect(screen.getByText('/apps/github')).toBeInTheDocument(); + expect(screen.getByText('No Owner')).toBeInTheDocument(); + expect(screen.getByText('src/utils/*')).toBeInTheDocument(); + expect(screen.getAllByText(user1.name).length).toBeGreaterThanOrEqual(1); + }); + + it('should show exclusion rules even when My Teams filter is active', async () => { + const codeownerRules: ParsedOwnershipRule[] = [ + { + matcher: {pattern: '/apps/github', type: 'codeowners'}, + owners: [], + }, + ]; + const projectRules: ParsedOwnershipRule[] = [ + { + matcher: {pattern: 'src/app/*', type: 'path'}, + owners: [{type: 'user', id: user1.id, name: user1.name}], + }, + ]; + + render( + + ); + + // Both rules should be visible with My Teams active (default) + expect(screen.getByText('/apps/github')).toBeInTheDocument(); + expect(screen.getByText('No Owner')).toBeInTheDocument(); + expect(await screen.findByText('src/app/*')).toBeInTheDocument(); + expect(screen.getAllByText(user1.name).length).toBeGreaterThanOrEqual(1); + }); + + it('should render multiple exclusion rules', async () => { + const codeownerRules: ParsedOwnershipRule[] = [ + { + matcher: {pattern: '/apps/github', type: 'codeowners'}, + owners: [], + }, + { + matcher: {pattern: '/vendor/*', type: 'codeowners'}, + owners: [], + }, + { + matcher: {pattern: '/build/*', type: 'codeowners'}, + owners: [], + }, + ]; + + render( + + ); + + expect(await screen.findByText('/apps/github')).toBeInTheDocument(); + expect(screen.getByText('/vendor/*')).toBeInTheDocument(); + expect(screen.getByText('/build/*')).toBeInTheDocument(); + expect(screen.getAllByText('No Owner')).toHaveLength(3); + }); }); diff --git a/static/app/views/settings/project/projectOwnership/ownershipRulesTable.tsx b/static/app/views/settings/project/projectOwnership/ownershipRulesTable.tsx index 1a190d3573ebf0..b6c665bb5defb4 100644 --- a/static/app/views/settings/project/projectOwnership/ownershipRulesTable.tsx +++ b/static/app/views/settings/project/projectOwnership/ownershipRulesTable.tsx @@ -8,6 +8,7 @@ import uniqBy from 'lodash/uniqBy'; import {Tag} from '@sentry/scraps/badge'; import {Button, ButtonBar} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; +import {Text} from '@sentry/scraps/text'; import {PanelTable} from 'sentry/components/panels/panelTable'; import {SearchBar} from 'sentry/components/searchBar'; @@ -129,6 +130,7 @@ export function OwnershipRulesTable({ (selectedActors === null || // Selected actors was cleared selectedActors.length === 0 || + rule.owners.length === 0 || rule.owners.some(owner => selectedActors.includes(`${owner.type}:${owner.id}`))) ); @@ -186,6 +188,7 @@ export function OwnershipRulesTable({ emptyMessage={t('No ownership rules found')} > {chunkedRules[page]?.map((rule, index) => { + const isExclusionRule = rule.owners.length === 0; const hasUnknownOwners = rule.owners.some(owner => !defined(owner.id)); const ownerNames = rule.owners.map(owner => { if (!owner.id) { @@ -208,20 +211,26 @@ export function OwnershipRulesTable({ {rule.matcher.pattern} - - {/* Avoid attempting to render the avatar stack if there are broken owners */} - {!hasUnknownOwners && ( - - )} - - {name} - {rule.owners.length > 1 && - tn(' and %s other', ' and %s others', rule.owners.length - 1)} + {isExclusionRule ? ( + {t('No Owner')} + ) : ( + + + {/* Avoid attempting to render the avatar stack if there are broken owners */} + {!hasUnknownOwners && ( + + )} + + {name} + {rule.owners.length > 1 && + tn(' and %s other', ' and %s others', rule.owners.length - 1)} + + )} );