import {useState} from 'react'; import styled from '@emotion/styled'; import Access from 'sentry/components/acl/access'; import ActorAvatar from 'sentry/components/avatar/actorAvatar'; import TeamAvatar from 'sentry/components/avatar/teamAvatar'; import Tag from 'sentry/components/badge/tag'; import {openConfirmModal} from 'sentry/components/confirm'; import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete'; import type {ItemsBeforeFilter} from 'sentry/components/dropdownAutoComplete/types'; import DropdownBubble from 'sentry/components/dropdownBubble'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import ErrorBoundary from 'sentry/components/errorBoundary'; import IdBadge from 'sentry/components/idBadge'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {IconChevron, IconEllipsis, IconUser} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {MonitorType} from 'sentry/types/alerts'; import type {Actor} from 'sentry/types/core'; import type {Project} from 'sentry/types/project'; import {useUserTeams} from 'sentry/utils/useUserTeams'; import ActivatedMetricAlertRuleStatus from 'sentry/views/alerts/list/rules/activatedMetricAlertRuleStatus'; import AlertLastIncidentActivationInfo from 'sentry/views/alerts/list/rules/alertLastIncidentActivationInfo'; import AlertRuleStatus from 'sentry/views/alerts/list/rules/alertRuleStatus'; import CombinedAlertBadge from 'sentry/views/alerts/list/rules/combinedAlertBadge'; import {getActor} from 'sentry/views/alerts/list/rules/utils'; import { UptimeMonitorMode, UptimeMonitorStatus, } from 'sentry/views/alerts/rules/uptime/types'; import type {CombinedAlerts} from '../../types'; import {CombinedAlertType} from '../../types'; import {isIssueAlert} from '../../utils'; type Props = { hasEditAccess: boolean; onDelete: (projectId: string, rule: CombinedAlerts) => void; onOwnerChange: (projectId: string, rule: CombinedAlerts, ownerValue: string) => void; orgId: string; projects: Project[]; projectsLoaded: boolean; rule: CombinedAlerts; }; function RuleListRow({ rule, projectsLoaded, projects, orgId, onDelete, onOwnerChange, hasEditAccess, }: Props) { const {teams: userTeams} = useUserTeams(); const [assignee, setAssignee] = useState(''); const isActivatedAlertRule = rule.type === CombinedAlertType.METRIC && rule.monitorType === MonitorType.ACTIVATED; const isUptime = rule.type === CombinedAlertType.UPTIME; const slug = isUptime ? rule.projectSlug : rule.projects[0]; const editKey = { [CombinedAlertType.ISSUE]: 'rules', [CombinedAlertType.METRIC]: 'metric-rules', [CombinedAlertType.UPTIME]: 'uptime-rules', } satisfies Record; const editLink = `/organizations/${orgId}/alerts/${editKey[rule.type]}/${slug}/${rule.id}/`; const mutateKey = { [CombinedAlertType.ISSUE]: 'issue', [CombinedAlertType.METRIC]: 'metric', [CombinedAlertType.UPTIME]: 'uptime', } satisfies Record; const duplicateLink = { pathname: `/organizations/${orgId}/alerts/new/${mutateKey[rule.type]}/`, query: { project: slug, duplicateRuleId: rule.id, createFromDuplicate: 'true', referrer: 'alert_stream', }, }; const ownerActor = getActor(rule); const canEdit = ownerActor?.id ? userTeams.some(team => team.id === ownerActor.id) : true; const activeActions = { [CombinedAlertType.ISSUE]: ['edit', 'duplicate', 'delete'], [CombinedAlertType.METRIC]: ['edit', 'duplicate', 'delete'], [CombinedAlertType.UPTIME]: ['edit', 'delete'], }; const actions: MenuItemProps[] = [ { key: 'edit', label: t('Edit'), to: editLink, hidden: !activeActions[rule.type].includes('edit'), }, { key: 'duplicate', label: t('Duplicate'), to: duplicateLink, hidden: !activeActions[rule.type].includes('duplicate'), }, { key: 'delete', label: t('Delete'), hidden: !activeActions[rule.type].includes('delete'), priority: 'danger', onAction: () => { openConfirmModal({ onConfirm: () => onDelete(slug, rule), header:
{t('Delete Alert Rule?')}
, message: t( 'Are you sure you want to delete "%s"? You won\'t be able to view the history of this alert once it\'s deleted.', rule.name ), confirmText: t('Delete Rule'), priority: 'danger', }); }, }, ]; function handleOwnerChange({value}: {value: string}) { const ownerValue = value && `team:${value}`; setAssignee(ownerValue); onOwnerChange(slug, rule, ownerValue); } const unassignedOption: ItemsBeforeFilter[number] = { value: '', label: ( ), searchKey: 'unassigned', actor: '', disabled: false, }; const project = projects.find(p => p.slug === slug); const filteredProjectTeams = (project?.teams ?? []).filter(projTeam => { return userTeams.some(team => team.id === projTeam.id); }); const dropdownTeams = filteredProjectTeams .map((team, idx) => ({ value: team.id, searchKey: team.slug, label: ( ), })) .concat(unassignedOption); const teamId = assignee?.split(':')[1]; const teamName = filteredProjectTeams.find(team => team.id === teamId); const assigneeTeamActor = assignee && { type: 'team' as Actor['type'], id: teamId, name: '', }; const avatarElement = assigneeTeamActor ? ( ) : ( ); const hasUptimeAutoconfigureBadge = rule.type === CombinedAlertType.UPTIME && rule.mode === UptimeMonitorMode.AUTO_DETECTED_ACTIVE; const titleBadge = hasUptimeAutoconfigureBadge ? ( ), } )} > {t('Auto Detected')} ) : null; return ( {rule.name} {titleBadge} {isActivatedAlertRule ? ( ) : isUptime ? ( rule.status === UptimeMonitorStatus.FAILED ? ( t('Down') ) : ( t('Up') ) ) : ( )} {ownerActor ? ( ) : ( {!projectsLoaded && } {projectsLoaded && ( { e?.stopPropagation(); }} items={dropdownTeams} alignMenu="right" onSelect={handleOwnerChange} itemSize="small" searchPlaceholder={t('Filter teams')} disableLabelPadding emptyHidesInput disabled={!hasEditAccess} > {({getActorProps, isOpen}) => ( {avatarElement} {hasEditAccess && ( )} )} )} )} {({hasAccess}) => ( , showChevron: false, }} disabledKeys={hasAccess && canEdit ? [] : ['delete']} /> )} ); } // TODO: see static/app/components/profiling/flex.tsx and utilize the FlexContainer styled component const FlexCenter = styled('div')` display: flex; align-items: center; `; const AlertNameWrapper = styled('div')<{isIssueAlert?: boolean}>` ${p => p.theme.overflowEllipsis} display: flex; align-items: center; gap: ${space(2)}; ${p => p.isIssueAlert && `padding: ${space(3)} ${space(2)}; line-height: 2.4;`} `; const AlertNameAndStatus = styled('div')` ${p => p.theme.overflowEllipsis} line-height: 1.35; `; const AlertName = styled('div')` ${p => p.theme.overflowEllipsis} font-size: ${p => p.theme.fontSizeLarge}; `; const AlertIncidentDate = styled('div')` color: ${p => p.theme.gray300}; `; const ProjectBadgeContainer = styled('div')` width: 100%; `; const ProjectBadge = styled(IdBadge)` flex-shrink: 0; `; const ActionsColumn = styled('div')` display: flex; align-items: center; justify-content: center; padding: ${space(1)}; `; const AssigneeWrapper = styled('div')` display: flex; justify-content: flex-end; /* manually align menu underneath dropdown caret */ ${DropdownBubble} { right: -14px; } `; const DropdownButton = styled('div')` display: flex; align-items: center; font-size: 20px; `; const StyledChevron = styled(IconChevron)` margin-left: ${space(1)}; `; const PaddedIconUser = styled(IconUser)` padding: ${space(0.25)}; `; const IconContainer = styled('div')` display: flex; align-items: center; justify-content: center; width: ${p => p.theme.iconSizes.lg}; height: ${p => p.theme.iconSizes.lg}; flex-shrink: 0; `; const MenuItemWrapper = styled('div')` display: flex; align-items: center; font-size: ${p => p.theme.fontSizeSmall}; `; const Label = styled(TextOverflow)` margin-left: ${space(0.75)}; `; const MarginLeft = styled('div')` margin-left: ${space(1)}; `; const StyledLoadingIndicator = styled(LoadingIndicator)` height: 24px; margin: 0; margin-right: ${space(1.5)}; `; export default RuleListRow;