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)}
+
+ )}
);