Skip to content

Commit 6bcb137

Browse files
authored
fix(aci): Include project alerts on monitor list page (#111690)
On the monitor list views we are only showing the directly-connected alerts. The detail pages already show the full list including project-connected alerts. This simply adds the functionality to the list view.
1 parent 93dc014 commit 6bcb137

File tree

4 files changed

+137
-12
lines changed

4 files changed

+137
-12
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {AutomationFixture} from 'sentry-fixture/automations';
2+
import {IssueStreamDetectorFixture} from 'sentry-fixture/detectors';
3+
4+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
5+
6+
import {DetectorListConnectedAutomations} from 'sentry/views/detectors/components/detectorListConnectedAutomations';
7+
8+
describe('DetectorListConnectedAutomations', () => {
9+
beforeEach(() => {
10+
MockApiClient.clearMockResponses();
11+
});
12+
13+
it('renders combined count of direct and project alerts', async () => {
14+
MockApiClient.addMockResponse({
15+
url: '/organizations/org-slug/workflows/',
16+
body: [
17+
AutomationFixture({id: '1', name: 'Direct Alert'}),
18+
AutomationFixture({id: '2', name: 'Project Alert'}),
19+
],
20+
});
21+
22+
MockApiClient.addMockResponse({
23+
url: '/organizations/org-slug/detectors/',
24+
body: [IssueStreamDetectorFixture({workflowIds: ['2']})],
25+
});
26+
27+
render(<DetectorListConnectedAutomations automationIds={['1']} projectId="1" />);
28+
29+
expect(await screen.findByText('2 alerts')).toBeInTheDocument();
30+
});
31+
32+
it('deduplicates automation ids from both sources', async () => {
33+
MockApiClient.addMockResponse({
34+
url: '/organizations/org-slug/workflows/',
35+
body: [AutomationFixture({id: '1', name: 'Shared Alert'})],
36+
});
37+
38+
MockApiClient.addMockResponse({
39+
url: '/organizations/org-slug/detectors/',
40+
body: [IssueStreamDetectorFixture({workflowIds: ['1']})],
41+
});
42+
43+
render(<DetectorListConnectedAutomations automationIds={['1']} projectId="1" />);
44+
45+
expect(await screen.findByText('1 alert')).toBeInTheDocument();
46+
});
47+
48+
it('renders empty cell when no automations are connected', async () => {
49+
MockApiClient.addMockResponse({
50+
url: '/organizations/org-slug/workflows/',
51+
body: [],
52+
});
53+
54+
MockApiClient.addMockResponse({
55+
url: '/organizations/org-slug/detectors/',
56+
body: [IssueStreamDetectorFixture({workflowIds: []})],
57+
});
58+
59+
render(<DetectorListConnectedAutomations automationIds={[]} projectId="1" />);
60+
61+
// EmptyCell renders an em dash
62+
expect(await screen.findByText('—')).toBeInTheDocument();
63+
});
64+
65+
it('shows automation names in hovercard on hover', async () => {
66+
MockApiClient.addMockResponse({
67+
url: '/organizations/org-slug/workflows/',
68+
body: [
69+
AutomationFixture({id: '1', name: 'Direct Alert'}),
70+
AutomationFixture({id: '2', name: 'Project Alert'}),
71+
],
72+
});
73+
74+
MockApiClient.addMockResponse({
75+
url: '/organizations/org-slug/detectors/',
76+
body: [IssueStreamDetectorFixture({workflowIds: ['2']})],
77+
});
78+
79+
render(<DetectorListConnectedAutomations automationIds={['1']} projectId="1" />);
80+
81+
await userEvent.hover(await screen.findByText('2 alerts'));
82+
83+
const directAlert = await screen.findByText('Direct Alert');
84+
const projectAlert = screen.getByText('Project Alert');
85+
86+
expect(directAlert.closest('a')).toHaveAttribute(
87+
'href',
88+
'/organizations/org-slug/monitors/alerts/1/'
89+
);
90+
expect(projectAlert.closest('a')).toHaveAttribute(
91+
'href',
92+
'/organizations/org-slug/monitors/alerts/2/'
93+
);
94+
});
95+
});

static/app/views/detectors/components/detectorListConnectedAutomations.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useMemo} from 'react';
12
import {ClassNames} from '@emotion/react';
23
import styled from '@emotion/styled';
34

@@ -15,9 +16,11 @@ import {AutomationActionSummary} from 'sentry/views/automations/components/autom
1516
import {useAutomationsQuery} from 'sentry/views/automations/hooks';
1617
import {getAutomationActions} from 'sentry/views/automations/hooks/utils';
1718
import {makeAutomationDetailsPathname} from 'sentry/views/automations/pathnames';
19+
import {useIssueStreamDetectorsForProject} from 'sentry/views/detectors/utils/useIssueStreamDetectorsForProject';
1820

1921
type DetectorListConnectedAutomationsProps = {
2022
automationIds: string[];
23+
projectId: string;
2124
};
2225

2326
function ConnectedAutomationsHoverBody({automationIds}: {automationIds: string[]}) {
@@ -27,6 +30,9 @@ function ConnectedAutomationsHoverBody({automationIds}: {automationIds: string[]
2730
ids: automationIds.slice(0, 5),
2831
});
2932
const hasMore = automationIds.length > 5;
33+
const hasMoreText = hasMore ? (
34+
<MoreText>{tn('%s more', '%s more', automationIds.length - 5)}</MoreText>
35+
) : null;
3036

3137
if (isError) {
3238
return <LoadingError />;
@@ -41,6 +47,7 @@ function ConnectedAutomationsHoverBody({automationIds}: {automationIds: string[]
4147
<Placeholder height="18px" width="40%" />
4248
</Stack>
4349
))}
50+
{hasMoreText}
4451
</div>
4552
);
4653
}
@@ -65,17 +72,32 @@ function ConnectedAutomationsHoverBody({automationIds}: {automationIds: string[]
6572
</HovercardRow>
6673
);
6774
})}
68-
{hasMore && (
69-
<MoreText>{tn('%s more', '%s more', automationIds.length - 5)}</MoreText>
70-
)}
75+
{hasMoreText}
7176
</div>
7277
);
7378
}
7479

7580
export function DetectorListConnectedAutomations({
7681
automationIds,
82+
projectId,
7783
}: DetectorListConnectedAutomationsProps) {
78-
if (!automationIds.length) {
84+
const {data: issueStreamDetectors, isPending} =
85+
useIssueStreamDetectorsForProject(projectId);
86+
87+
// Combine the automation IDs from the project's issue stream detector with the directly-connected ones
88+
const combinedAutomationIds = useMemo(() => {
89+
if (isPending) {
90+
return automationIds;
91+
}
92+
const issueStreamAutomationIds = issueStreamDetectors?.[0]?.workflowIds ?? [];
93+
return [...new Set([...automationIds, ...issueStreamAutomationIds])];
94+
}, [automationIds, issueStreamDetectors, isPending]);
95+
96+
if (isPending) {
97+
return <Placeholder height="20px" />;
98+
}
99+
100+
if (!combinedAutomationIds.length) {
79101
return <EmptyCell />;
80102
}
81103

@@ -84,13 +106,13 @@ export function DetectorListConnectedAutomations({
84106
<ClassNames>
85107
{({css}) => (
86108
<Hovercard
87-
body={<ConnectedAutomationsHoverBody automationIds={automationIds} />}
109+
body={<ConnectedAutomationsHoverBody automationIds={combinedAutomationIds} />}
88110
bodyClassName={css`
89111
padding: 0;
90112
`}
91113
showUnderline
92114
>
93-
{tn('%s alert', '%s alerts', automationIds.length)}
115+
{tn('%s alert', '%s alerts', combinedAutomationIds.length)}
94116
</Hovercard>
95117
)}
96118
</ClassNames>

static/app/views/detectors/components/detectorListTable/detectorListRow.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export function DetectorListRow({detector, selected, onSelect}: DetectorListRowP
5252
<DetectorAssigneeCell assignee={detector.owner} />
5353
</SimpleTable.RowCell>
5454
<SimpleTable.RowCell data-column-name="connected-automations">
55-
<DetectorListConnectedAutomations automationIds={detector.workflowIds} />
55+
<DetectorListConnectedAutomations
56+
automationIds={detector.workflowIds}
57+
projectId={detector.projectId}
58+
/>
5659
</SimpleTable.RowCell>
5760
{additionalColumns.map(col => (
5861
<Fragment key={col.id}>{col.renderCell(detector)}</Fragment>

static/app/views/detectors/utils/useIssueStreamDetectorsForProject.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
55
* Issue stream detectors are used to connect automations to "all issues in a project".
66
*/
77
export function useIssueStreamDetectorsForProject(projectId: string | undefined) {
8-
return useDetectorsQuery({
9-
query: 'type:issue_stream',
10-
projects: [Number(projectId)],
11-
includeIssueStreamDetectors: true,
12-
});
8+
return useDetectorsQuery(
9+
{
10+
query: 'type:issue_stream',
11+
projects: [Number(projectId)],
12+
includeIssueStreamDetectors: true,
13+
},
14+
{
15+
staleTime: Infinity,
16+
}
17+
);
1318
}

0 commit comments

Comments
 (0)