forked from getsentry/sentry
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(toolbar): Add a panel to show active alerts (getsentry#74657)
New panel to reveal active alerts in your sentry instance: <img width="383" alt="SCR-20240722-kpuw" src="https://github.com/user-attachments/assets/02a88f29-5882-4aac-8323-9d9d6301400e">
- Loading branch information
Showing
16 changed files
with
313 additions
and
28 deletions.
There are no files selected for viewing
185 changes: 185 additions & 0 deletions
185
static/app/components/devtoolbar/components/alerts/alertsPanel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
import {css} from '@emotion/react'; | ||
|
||
import ActorAvatar from 'sentry/components/avatar/actorAvatar'; | ||
import AlertBadge from 'sentry/components/badge/alertBadge'; | ||
import ProjectBadge from 'sentry/components/idBadge/projectBadge'; | ||
import Placeholder from 'sentry/components/placeholder'; | ||
import TextOverflow from 'sentry/components/textOverflow'; | ||
import TimeSince from 'sentry/components/timeSince'; | ||
import type {Actor} from 'sentry/types/core'; | ||
import type {Organization} from 'sentry/types/organization'; | ||
import AlertRuleStatus from 'sentry/views/alerts/list/rules/alertRuleStatus'; | ||
import type {Incident, MetricAlert} from 'sentry/views/alerts/types'; | ||
import {CombinedAlertType} from 'sentry/views/alerts/types'; | ||
import {alertDetailsLink} from 'sentry/views/alerts/utils'; | ||
|
||
import useConfiguration from '../../hooks/useConfiguration'; | ||
import { | ||
badgeWithLabelCss, | ||
gridFlexEndCss, | ||
listItemGridCss, | ||
listItemPlaceholderWrapperCss, | ||
} from '../../styles/listItem'; | ||
import {panelInsetContentCss, panelSectionCss} from '../../styles/panel'; | ||
import {resetFlexColumnCss, resetFlexRowCss} from '../../styles/reset'; | ||
import {smallCss, xSmallCss} from '../../styles/typography'; | ||
import InfiniteListItems from '../infiniteListItems'; | ||
import InfiniteListState from '../infiniteListState'; | ||
import PanelLayout from '../panelLayout'; | ||
import SentryAppLink from '../sentryAppLink'; | ||
import useTeams from '../teams/useTeams'; | ||
|
||
import useInfiniteAlertsList from './useInfiniteAlertsList'; | ||
|
||
export default function AlertsPanel() { | ||
const {projectId, projectSlug, trackAnalytics} = useConfiguration(); | ||
const queryResult = useInfiniteAlertsList(); | ||
|
||
const estimateSize = 84; | ||
const placeholderHeight = `${estimateSize - 8}px`; // The real height of the items, minus the padding-block value | ||
|
||
return ( | ||
<PanelLayout title="Alerts"> | ||
<div css={[smallCss, panelSectionCss, panelInsetContentCss]}> | ||
<span css={[resetFlexRowCss, {gap: 'var(--space50)'}]}> | ||
Active Alerts in{' '} | ||
<SentryAppLink | ||
to={{url: `/projects/${projectSlug}/`}} | ||
onClick={() => { | ||
trackAnalytics?.({ | ||
eventKey: `devtoolbar.alerts-list.header.click`, | ||
eventName: `devtoolbar: Click alert-list header`, | ||
}); | ||
}} | ||
> | ||
<div css={[resetFlexRowCss, {display: 'inline-flex', gap: 'var(--space50)'}]}> | ||
<ProjectBadge | ||
css={css({'&& img': {boxShadow: 'none'}})} | ||
project={{slug: projectSlug, id: projectId}} | ||
avatarSize={16} | ||
hideName | ||
avatarProps={{hasTooltip: false}} | ||
/> | ||
{projectSlug} | ||
</div> | ||
</SentryAppLink> | ||
</span> | ||
</div> | ||
|
||
<div css={resetFlexColumnCss}> | ||
<InfiniteListState | ||
queryResult={queryResult} | ||
backgroundUpdatingMessage={() => null} | ||
loadingMessage={() => ( | ||
<div | ||
css={[ | ||
resetFlexColumnCss, | ||
panelSectionCss, | ||
panelInsetContentCss, | ||
listItemPlaceholderWrapperCss, | ||
]} | ||
> | ||
<Placeholder height={placeholderHeight} /> | ||
<Placeholder height={placeholderHeight} /> | ||
<Placeholder height={placeholderHeight} /> | ||
<Placeholder height={placeholderHeight} /> | ||
</div> | ||
)} | ||
> | ||
<InfiniteListItems | ||
estimateSize={() => estimateSize} | ||
queryResult={queryResult} | ||
itemRenderer={props => <AlertListItem {...props} />} | ||
emptyMessage={() => <p css={panelInsetContentCss}>No items to show</p>} | ||
/> | ||
</InfiniteListState> | ||
</div> | ||
</PanelLayout> | ||
); | ||
} | ||
|
||
function AlertListItem({item}: {item: Incident}) { | ||
const {organizationSlug, trackAnalytics} = useConfiguration(); | ||
|
||
const ownerId = item.alertRule.owner?.split(':').at(1); | ||
|
||
const {data: teams} = useTeams( | ||
{idOrSlug: String(ownerId)}, | ||
{enabled: Boolean(ownerId)} | ||
); | ||
const ownerTeam = teams?.json.at(0); | ||
|
||
const teamActor = ownerId | ||
? {type: 'team' as Actor['type'], id: ownerId, name: ownerTeam?.name ?? ''} | ||
: null; | ||
|
||
const rule: MetricAlert = { | ||
type: CombinedAlertType.METRIC, | ||
...item.alertRule, | ||
latestIncident: item, | ||
}; | ||
|
||
return ( | ||
<div | ||
css={[ | ||
listItemGridCss, | ||
css` | ||
grid-template-areas: | ||
'badge name time' | ||
'badge message message' | ||
'. icons icons'; | ||
grid-template-columns: max-content 1fr max-content; | ||
gap: var(--space25) var(--space100); | ||
`, | ||
]} | ||
> | ||
<div style={{gridArea: 'badge'}}> | ||
<AlertBadge status={item.status} isIssue={false} /> | ||
</div> | ||
|
||
<div | ||
css={[gridFlexEndCss, xSmallCss]} | ||
style={{gridArea: 'time', color: 'var(--gray300)'}} | ||
> | ||
<TimeSince date={item.dateStarted} unitStyle="extraShort" /> | ||
</div> | ||
|
||
<TextOverflow css={smallCss} style={{gridArea: 'name'}}> | ||
<SentryAppLink | ||
to={{ | ||
url: alertDetailsLink({slug: organizationSlug} as Organization, item), | ||
query: {alert: item.identifier}, | ||
}} | ||
onClick={() => { | ||
trackAnalytics?.({ | ||
eventKey: `devtoolbar.alert-list.item.click`, | ||
eventName: `devtoolbar: Click alert-list item`, | ||
}); | ||
}} | ||
> | ||
<strong>{item.title}</strong> | ||
</SentryAppLink> | ||
</TextOverflow> | ||
|
||
<div css={smallCss} style={{gridArea: 'message'}}> | ||
<AlertRuleStatus rule={rule} /> | ||
</div> | ||
|
||
{teamActor ? ( | ||
<div | ||
css={[ | ||
badgeWithLabelCss, | ||
xSmallCss, | ||
css` | ||
justify-self: flex-end; | ||
`, | ||
]} | ||
style={{gridArea: 'icons'}} | ||
> | ||
<ActorAvatar actor={teamActor} size={16} hasTooltip={false} />{' '} | ||
<TextOverflow>{teamActor.name}</TextOverflow> | ||
</div> | ||
) : null} | ||
</div> | ||
); | ||
} |
30 changes: 30 additions & 0 deletions
30
static/app/components/devtoolbar/components/alerts/useInfiniteAlertsList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import {useMemo} from 'react'; | ||
|
||
import type {Incident} from 'sentry/views/alerts/types'; | ||
|
||
import useConfiguration from '../../hooks/useConfiguration'; | ||
import useFetchInfiniteApiData from '../../hooks/useFetchInfiniteApiData'; | ||
import type {ApiEndpointQueryKey} from '../../types'; | ||
|
||
export default function useInfiniteFeedbackList() { | ||
const {organizationSlug, projectId} = useConfiguration(); | ||
|
||
return useFetchInfiniteApiData<Incident[]>({ | ||
queryKey: useMemo( | ||
(): ApiEndpointQueryKey => [ | ||
'io.sentry.toolbar', | ||
`/organizations/${organizationSlug}/incidents/`, | ||
{ | ||
query: { | ||
limit: 25, | ||
queryReferrer: 'devtoolbar', | ||
project: [projectId], | ||
statsPeriod: '14d', | ||
status: 'open', | ||
}, | ||
}, | ||
], | ||
[organizationSlug, projectId] | ||
), | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
static/app/components/devtoolbar/components/teams/useTeams.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import {useMemo} from 'react'; | ||
|
||
import type {Team} from 'sentry/types/organization'; | ||
|
||
import useConfiguration from '../../hooks/useConfiguration'; | ||
import useFetchApiData from '../../hooks/useFetchApiData'; | ||
import type {ApiEndpointQueryKey} from '../../types'; | ||
|
||
interface Props { | ||
idOrSlug?: string; | ||
} | ||
|
||
export default function useTeams({idOrSlug}: Props, opts?: {enabled: boolean}) { | ||
const {organizationSlug} = useConfiguration(); | ||
|
||
return useFetchApiData<Team[]>({ | ||
queryKey: useMemo( | ||
(): ApiEndpointQueryKey => [ | ||
'io.sentry.toolbar', | ||
`/organizations/${organizationSlug}/teams/`, | ||
{ | ||
query: { | ||
query: `id:${idOrSlug}`, | ||
}, | ||
}, | ||
], | ||
[idOrSlug, organizationSlug] | ||
), | ||
cacheTime: Infinity, | ||
enabled: opts?.enabled ?? true, | ||
}); | ||
} |
Oops, something went wrong.