import {Component} from 'react'; import {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import isEmpty from 'lodash/isEmpty'; import uniq from 'lodash/uniq'; import {addErrorMessage, addMessage} from 'sentry/actionCreators/indicator'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import * as Layout from 'sentry/components/layouts/thirds'; import Link from 'sentry/components/links/link'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import Pagination from 'sentry/components/pagination'; import PanelTable from 'sentry/components/panels/panelTable'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; import {Organization, PageFilters, Project} from 'sentry/types'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import Projects from 'sentry/utils/projects'; import Teams from 'sentry/utils/teams'; import withPageFilters from 'sentry/utils/withPageFilters'; import FilterBar from '../../filterBar'; import {AlertRuleType, CombinedMetricIssueAlerts} from '../../types'; import {getTeamParams, isIssueAlert} from '../../utils'; import AlertHeader from '../header'; import RuleListRow from './row'; type Props = RouteComponentProps<{}, {}> & { organization: Organization; selection: PageFilters; }; type State = { ruleList?: Array | null; teamFilterSearch?: string; }; class AlertRulesList extends DeprecatedAsyncComponent< Props, State & DeprecatedAsyncComponent['state'] > { getEndpoints(): ReturnType { const {organization, location} = this.props; const {query} = location; query.expand = ['latestIncident', 'lastTriggered']; query.team = getTeamParams(query.team); if (!query.sort) { query.sort = ['incident_status', 'date_triggered']; } return [ [ 'ruleList', `/organizations/${organization.slug}/combined-rules/`, { query, }, ], ]; } handleChangeFilter = (activeFilters: string[]) => { const {router, location} = this.props; const {cursor: _cursor, page: _page, ...currentQuery} = location.query; router.push({ pathname: location.pathname, query: { ...currentQuery, team: activeFilters.length > 0 ? activeFilters : '', }, }); }; handleChangeSearch = (name: string) => { const {router, location} = this.props; const {cursor: _cursor, page: _page, ...currentQuery} = location.query; router.push({ pathname: location.pathname, query: { ...currentQuery, name, }, }); }; handleOwnerChange = ( projectId: string, rule: CombinedMetricIssueAlerts, ownerValue: string ) => { const {organization} = this.props; const alertPath = rule.type === 'alert_rule' ? 'alert-rules' : 'rules'; const endpoint = `/projects/${organization.slug}/${projectId}/${alertPath}/${rule.id}/`; const updatedRule = {...rule, owner: ownerValue}; this.api.request(endpoint, { method: 'PUT', data: updatedRule, success: () => { addMessage(t('Updated alert rule'), 'success'); }, error: () => { addMessage(t('Unable to save change'), 'error'); }, }); }; handleDeleteRule = async (projectId: string, rule: CombinedMetricIssueAlerts) => { const {organization} = this.props; const alertPath = isIssueAlert(rule) ? 'rules' : 'alert-rules'; try { await this.api.requestPromise( `/projects/${organization.slug}/${projectId}/${alertPath}/${rule.id}/`, { method: 'DELETE', } ); this.reloadData(); } catch (_err) { addErrorMessage(t('Error deleting rule')); } }; renderLoading() { return this.renderBody(); } renderList() { const {location, organization, router} = this.props; const {loading, ruleListPageLinks} = this.state; const {query} = location; const hasEditAccess = organization.access.includes('alerts:write'); const ruleList = (this.state.ruleList ?? []).filter(defined); const projectsFromResults = uniq(ruleList.flatMap(({projects}) => projects)); const sort: { asc: boolean; field: 'date_added' | 'name' | ['incident_status', 'date_triggered']; } = { asc: query.asc === '1', field: query.sort || 'date_added', }; const {cursor: _cursor, page: _page, ...currentQuery} = query; const isAlertRuleSort = sort.field.includes('incident_status') || sort.field.includes('date_triggered'); const sortArrow = ( ); return ( {({initiallyLoaded: loadedTeams, teams}) => ( {t('Alert Rule')} {sort.field === 'name' && sortArrow} , {t('Status')} {isAlertRuleSort && sortArrow} , t('Project'), t('Team'), t('Actions'), ]} isLoading={loading || !loadedTeams} isEmpty={ruleList.length === 0} emptyMessage={t('No alert rules found for the current query.')} > {({initiallyLoaded, projects}) => ruleList.map(rule => ( team.id))} hasEditAccess={hasEditAccess} /> )) } )} { let team = currentQuery.team; // Keep team parameter, but empty to remove parameters if (!team || team.length === 0) { team = ''; } router.push({ pathname: path, query: {...currentQuery, team, cursor}, }); }} /> ); } renderBody() { const {organization, router} = this.props; return ( {this.renderList()} ); } } class AlertRulesListContainer extends Component { componentDidMount() { this.trackView(); } componentDidUpdate(prevProps: Props) { const {location} = this.props; if (prevProps.location.query?.sort !== location.query?.sort) { this.trackView(); } } trackView() { const {organization, location} = this.props; trackAnalytics('alert_rules.viewed', { organization, sort: Array.isArray(location.query.sort) ? location.query.sort.join(',') : location.query.sort, }); } render() { return ; } } export default withPageFilters(AlertRulesListContainer); const StyledSortLink = styled(Link)` color: inherit; :hover { color: inherit; } `; const StyledPanelTable = styled(PanelTable)` position: static; overflow: auto; @media (min-width: ${p => p.theme.breakpoints.small}) { overflow: initial; } grid-template-columns: 4fr auto 140px 60px auto; white-space: nowrap; font-size: ${p => p.theme.fontSizeMedium}; `;