import {Fragment, useEffect} from 'react'; import styled from '@emotion/styled'; import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts'; import Feature from 'sentry/components/acl/feature'; import {Alert} from 'sentry/components/alert'; import {LinkButton} from 'sentry/components/button'; import CreateAlertButton from 'sentry/components/createAlertButton'; import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import * as Layout from 'sentry/components/layouts/thirds'; import ExternalLink from 'sentry/components/links/externalLink'; 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 {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import Projects from 'sentry/utils/projects'; import FilterBar from '../../filterBar'; import type {Incident} from '../../types'; import {getQueryStatus, getTeamParams} from '../../utils'; import AlertHeader from '../header'; import Onboarding from '../onboarding'; import AlertListRow from './row'; const DOCS_URL = 'https://docs.sentry.io/workflow/alerts-notifications/alerts/?_ga=2.21848383.580096147.1592364314-1444595810.1582160976'; type Props = RouteComponentProps<{}, {}> & { organization: Organization; }; type State = { incidentList: Incident[]; /** * User has not yet seen the 'alert_stream' welcome prompt for this * organization. */ firstVisitShown?: boolean; /** * Is there at least one alert rule configured for the currently selected * projects? */ hasAlertRule?: boolean; }; class IncidentsList extends DeprecatedAsyncComponent< Props, State & DeprecatedAsyncComponent['state'] > { getEndpoints(): ReturnType { const {organization, location} = this.props; const {query} = location; const status = getQueryStatus(query.status); return [ [ 'incidentList', `/organizations/${organization.slug}/incidents/`, { query: { ...query, status: status === 'all' ? undefined : status, team: getTeamParams(query.team), expand: ['original_alert_rule'], }, }, ], ]; } /** * If our incidentList is empty, determine if we've configured alert rules or * if the user has seen the welcome prompt. */ async onLoadAllEndpointsSuccess() { const {incidentList} = this.state; if (!incidentList || incidentList.length !== 0) { this.setState({hasAlertRule: true, firstVisitShown: false}); return; } this.setState({loading: true}); // Check if they have rules or not, to know which empty state message to // display const {location, organization} = this.props; const alertRules = await this.api.requestPromise( `/organizations/${organization.slug}/alert-rules/`, { method: 'GET', query: location.query, } ); const hasAlertRule = alertRules.length > 0; // We've already configured alert rules, no need to check if we should show // the "first time welcome" prompt if (hasAlertRule) { this.setState({hasAlertRule, firstVisitShown: false, loading: false}); return; } // Check if they have already seen the prompt for the alert stream const prompt = await promptsCheck(this.api, { organization, feature: 'alert_stream', }); const firstVisitShown = !prompt?.dismissedTime; if (firstVisitShown) { // Prompt has not been seen, mark the prompt as seen immediately so they // don't see it again promptsUpdate(this.api, { organization, feature: 'alert_stream', status: 'dismissed', }); } this.setState({hasAlertRule, firstVisitShown, loading: false}); } get projectsFromIncidents() { const {incidentList} = this.state; return [...new Set(incidentList?.flatMap(({projects}) => projects))]; } handleChangeSearch = (title: string) => { const {router, location} = this.props; const {cursor: _cursor, page: _page, ...currentQuery} = location.query; router.push({ pathname: location.pathname, query: { ...currentQuery, title, }, }); }; handleChangeFilter = (activeFilters: string[]) => { const {router, location} = this.props; const {cursor: _cursor, page: _page, ...currentQuery} = location.query; router.push({ pathname: location.pathname, query: { ...currentQuery, // Preserve empty team query parameter team: activeFilters.length > 0 ? activeFilters : '', }, }); }; handleChangeStatus = (value: string): void => { const {router, location} = this.props; const {cursor: _cursor, page: _page, ...currentQuery} = location.query; router.push({ pathname: location.pathname, query: { ...currentQuery, status: value === 'all' ? undefined : value, }, }); }; tryRenderOnboarding() { const {firstVisitShown} = this.state; const {organization} = this.props; if (!firstVisitShown) { return null; } const actions = ( {t('View Features')} {t('Create Alert')} ); return ; } renderLoading() { return this.renderBody(); } renderList() { const {loading, incidentList, incidentListPageLinks, hasAlertRule} = this.state; const {organization} = this.props; const checkingForAlertRules = incidentList?.length === 0 && hasAlertRule === undefined; const showLoadingIndicator = loading || checkingForAlertRules; return ( {this.tryRenderOnboarding() ?? ( {tct('Learn more about [link:Metric Alerts]', { link: , })} } headers={[ t('Alert Rule'), t('Triggered'), t('Duration'), t('Project'), t('Alert ID'), t('Team'), ]} > {({initiallyLoaded, projects}) => incidentList.map(incident => ( )) } )} ); } renderBody() { const {organization, router, location} = this.props; return ( {!this.tryRenderOnboarding() && ( {t('This page only shows metric alerts.')} )} {this.renderList()} ); } } function IncidentsListContainer(props: Props) { useEffect(() => { trackAnalytics('alert_stream.viewed', { organization: props.organization, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderDisabled = () => ( {t("You don't have access to this feature")} ); return ( ); } const StyledPanelTable = styled(PanelTable)` font-size: ${p => p.theme.fontSizeMedium}; & > div { padding: ${space(1.5)} ${space(2)}; } `; const StyledAlert = styled(Alert)` margin-bottom: ${space(1.5)}; `; const EmptyStateAction = styled('p')` font-size: ${p => p.theme.fontSizeLarge}; `; export default IncidentsListContainer;