Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<OwnershipRulesTable
projectRules={[]}
codeowners={[CodeOwnerFixture({schema: {rules: codeownerRules, version: 1}})]}
/>
);

// 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(
<OwnershipRulesTable
projectRules={projectRules}
codeowners={[CodeOwnerFixture({schema: {rules: codeownerRules, version: 1}})]}
/>
);

// 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(
<OwnershipRulesTable
projectRules={[]}
codeowners={[CodeOwnerFixture({schema: {rules: codeownerRules, version: 1}})]}
/>
);

expect(await screen.findByText('/apps/github')).toBeInTheDocument();
expect(screen.getByText('/vendor/*')).toBeInTheDocument();
expect(screen.getByText('/build/*')).toBeInTheDocument();
expect(screen.getAllByText('No Owner')).toHaveLength(3);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}`)))
);

Expand Down Expand Up @@ -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) {
Expand All @@ -208,20 +211,26 @@ export function OwnershipRulesTable({
</Flex>
<RowRule>{rule.matcher.pattern}</RowRule>
<Flex align="center" gap="md">
<AvatarContainer numAvatars={Math.min(rule.owners.length, 3)}>
{/* Avoid attempting to render the avatar stack if there are broken owners */}
{!hasUnknownOwners && (
<SuggestedAvatarStack
owners={rule.owners as Actor[]}
suggested={false}
reverse={false}
tooltip={ownerNames.join(', ')}
/>
)}
</AvatarContainer>
{name}
{rule.owners.length > 1 &&
tn(' and %s other', ' and %s others', rule.owners.length - 1)}
{isExclusionRule ? (
<Text variant="muted">{t('No Owner')}</Text>
) : (
<Fragment>
<AvatarContainer numAvatars={Math.min(rule.owners.length, 3)}>
{/* Avoid attempting to render the avatar stack if there are broken owners */}
{!hasUnknownOwners && (
<SuggestedAvatarStack
owners={rule.owners as Actor[]}
suggested={false}
reverse={false}
tooltip={ownerNames.join(', ')}
/>
)}
</AvatarContainer>
{name}
{rule.owners.length > 1 &&
tn(' and %s other', ' and %s others', rule.owners.length - 1)}
</Fragment>
)}
</Flex>
</Fragment>
);
Expand Down
Loading