diff --git a/static/gsApp/overrides/spendVisibility/enhancedUsageStatsOrganization.tsx b/static/gsApp/overrides/spendVisibility/enhancedUsageStatsOrganization.tsx index bcbce349cd53..012ba1967f68 100644 --- a/static/gsApp/overrides/spendVisibility/enhancedUsageStatsOrganization.tsx +++ b/static/gsApp/overrides/spendVisibility/enhancedUsageStatsOrganization.tsx @@ -37,7 +37,7 @@ import {withSubscription} from 'getsentry/components/withSubscription'; import {type Subscription} from 'getsentry/types'; import {SPIKE_PROTECTION_OPTION_DISABLED} from 'getsentry/views/spikeProtection/constants'; import {SpikeProtectionRangeLimitation} from 'getsentry/views/spikeProtection/spikeProtectionCallouts'; -import SpikeProtectionHistoryTable from 'getsentry/views/spikeProtection/spikeProtectionHistoryTable'; +import {SpikeProtectionHistoryTable} from 'getsentry/views/spikeProtection/spikeProtectionHistoryTable'; import SpikeProtectionUsageChart from 'getsentry/views/spikeProtection/spikeProtectionUsageChart'; import type { Spike, diff --git a/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.spec.tsx b/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.spec.tsx index 2e183fb17821..a890701cba37 100644 --- a/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.spec.tsx +++ b/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.spec.tsx @@ -8,7 +8,7 @@ import {DATA_CATEGORY_INFO} from 'sentry/constants'; import {DataCategoryExact} from 'sentry/types/core'; import {SubscriptionStore} from 'getsentry/stores/subscriptionStore'; -import SpikeProtectionHistoryTable from 'getsentry/views/spikeProtection/spikeProtectionHistoryTable'; +import {SpikeProtectionHistoryTable} from 'getsentry/views/spikeProtection/spikeProtectionHistoryTable'; import type {SpikeDetails} from 'getsentry/views/spikeProtection/types'; import {SPIKE_PROTECTION_OPTION_DISABLED} from './constants'; @@ -36,7 +36,7 @@ describe('SpikeProtectionHistoryTable', () => { }); }); - it('renders an empty state when no spikes are provided', async () => { + it('renders an empty state when no spikes are provided', () => { render( { {organization} ); - const emptyState = await screen.findByTestId('spike-history-empty'); + const emptyState = screen.getByTestId('spike-history-empty'); expect(emptyState).toBeInTheDocument(); const emptyMessage = screen.getByText(/No Significant Spikes/); expect(emptyMessage).toBeInTheDocument(); @@ -101,7 +101,7 @@ describe('SpikeProtectionHistoryTable', () => { />, {organization} ); - await screen.findByTestId('spike-protection-history-table'); + screen.getByTestId('spike-protection-history-table'); screen.getByText('2wk'); screen.getByText('1.3M'); screen.getByText('500K'); @@ -125,7 +125,7 @@ describe('SpikeProtectionHistoryTable', () => { ); }); - it('renders ongoing stored spike', async () => { + it('renders ongoing stored spike', () => { const storedSpikes: SpikeDetails[] = [ { start: new Date(2022, 0, 2, 6, 0, 0, 0).toISOString(), @@ -145,7 +145,7 @@ describe('SpikeProtectionHistoryTable', () => { {organization} ); - await screen.findByTestId('spike-protection-history-table'); + screen.getByTestId('spike-protection-history-table'); screen.getByText('Ongoing'); screen.getByText('200K'); screen.getByText('Jan 2, 2022 - present'); diff --git a/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.tsx b/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.tsx index 91852254beb5..da27e78090d5 100644 --- a/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.tsx +++ b/static/gsApp/views/spikeProtection/spikeProtectionHistoryTable.tsx @@ -1,10 +1,10 @@ -import {Component} from 'react'; import styled from '@emotion/styled'; import {Button, LinkButton} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import type {TableColumnConfig} from '@sentry/scraps/table'; +import {Text} from '@sentry/scraps/text'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {DiscoverButton} from 'sentry/components/discoverButton'; @@ -17,14 +17,12 @@ import {IconSettings} from 'sentry/icons'; import {IconTelescope} from 'sentry/icons/iconTelescope'; import {t, tct} from 'sentry/locale'; import type {DataCategoryInfo} from 'sentry/types/core'; -import type {Organization} from 'sentry/types/organization'; import type {ProjectSummaryWithOptions} from 'sentry/types/project'; import {defined} from 'sentry/utils/defined'; import {getExactDuration} from 'sentry/utils/duration/getExactDuration'; import {decodeScalar} from 'sentry/utils/queryString'; import {useApi} from 'sentry/utils/useApi'; import {useOrganization} from 'sentry/utils/useOrganization'; -import {withOrganization} from 'sentry/utils/withOrganization'; import {makeDiscoverPathname} from 'sentry/views/discover/pathnames'; import {getDiscoverDeprecation} from 'sentry/views/discover/utils'; import { @@ -32,8 +30,7 @@ import { getFormatUsageOptions, } from 'sentry/views/organizationStats/utils'; -import {withSubscription} from 'getsentry/components/withSubscription'; -import type {Subscription} from 'getsentry/types'; +import {useSubscription} from 'getsentry/hooks/useSubscription'; import { SpendVisibilityEvents, trackSpendVisibilityAnaltyics, @@ -50,10 +47,8 @@ import {isSpikeProtectionEnabled} from './spikeProtectionProjectToggle'; type Props = { dataCategoryInfo: DataCategoryInfo; onEnableSpikeProtection: () => void; - organization: Organization; project: ProjectSummaryWithOptions; spikes: SpikeDetails[]; - subscription: Subscription; isLoading?: boolean; }; @@ -68,15 +63,14 @@ const SPIKE_COLUMNS: TableColumnConfig[] = [ function EnableSpikeProtectionButton({ onEnableSpikeProtection, project, - subscription, ...props }: { onEnableSpikeProtection: () => void; project: ProjectSummaryWithOptions; - subscription: Subscription; }) { const api = useApi(); const organization = useOrganization(); + const subscription = useSubscription(); const endpoint = `/organizations/${organization.slug}/spike-protections/`; async function enableSpikeProtection() { @@ -93,7 +87,7 @@ function EnableSpikeProtectionButton({ ); trackSpendVisibilityAnaltyics(SpendVisibilityEvents.SP_PROJECT_TOGGLED, { organization, - subscription, + subscription: subscription ?? undefined, project_id: project.id, value: true, view: 'project_stats', @@ -118,105 +112,111 @@ function EnableSpikeProtectionButton({ ); } -class SpikeProtectionHistoryTable extends Component { - headers = [ - t('Past Spikes'), - t('Initial Threshold'), - t('Duration'), - t('Events Dropped'), - null, // Discover Query button - ]; +const HEADERS = [ + t('Past Spikes'), + t('Initial Threshold'), + t('Duration'), + t('Events Dropped'), + null, // Discover Query button +]; - renderSpikeRow(spike: SpikeDetails) { - const {dataCategoryInfo, project, organization, subscription} = this.props; - // ms -> s, rounds up to get duration in minutes - // rounding up to match the formatted date and time values - const millisecondsPerSecond = 1000; - const secondsPerMinute = 60; - const duration = spike.end - ? Math.ceil( - (new Date(spike.end).valueOf() - new Date(spike.start).valueOf()) / - (millisecondsPerSecond * secondsPerMinute) - ) * secondsPerMinute - : null; - return ( - - - - - - {defined(spike.threshold) - ? formatUsageWithUnits( - spike.threshold, - dataCategoryInfo.plural, - getFormatUsageOptions(dataCategoryInfo.plural) - ) - : '-'} - - - {duration ? getExactDuration(duration, true) : t('Ongoing')} - - - {spike.dropped - ? formatUsageWithUnits( - spike.dropped, - dataCategoryInfo.plural, - getFormatUsageOptions(dataCategoryInfo.plural) - ) - : '-'} - - - } - data-test-id="spike-protection-discover-button" - onClick={() => - trackSpendVisibilityAnaltyics(SpendVisibilityEvents.SP_DISCOVER_CLICKED, { - organization, - subscription, - view: 'project_stats', - }) - } - to={{ - pathname: makeDiscoverPathname({ - organization, - path: '/homepage/', - }), - query: { - project: [project.id], - start: decodeScalar(spike.start), - end: decodeScalar(spike.end), - }, - }} - > - {getDiscoverDeprecation(organization) - ? t('Open in Explore') - : t('Open in Discover')} - - - - ); - } +function SpikeRow({ + dataCategoryInfo, + project, + spike, +}: { + dataCategoryInfo: DataCategoryInfo; + project: ProjectSummaryWithOptions; + spike: SpikeDetails; +}) { + const organization = useOrganization(); + const subscription = useSubscription(); + // ms -> s, rounds up to get duration in minutes + // rounding up to match the formatted date and time values + const millisecondsPerSecond = 1000; + const secondsPerMinute = 60; + const duration = spike.end + ? Math.ceil( + (new Date(spike.end).valueOf() - new Date(spike.start).valueOf()) / + (millisecondsPerSecond * secondsPerMinute) + ) * secondsPerMinute + : null; + return ( + + + + + + {defined(spike.threshold) + ? formatUsageWithUnits( + spike.threshold, + dataCategoryInfo.plural, + getFormatUsageOptions(dataCategoryInfo.plural) + ) + : '-'} + + + {duration ? getExactDuration(duration, true) : t('Ongoing')} + + + {spike.dropped + ? formatUsageWithUnits( + spike.dropped, + dataCategoryInfo.plural, + getFormatUsageOptions(dataCategoryInfo.plural) + ) + : '-'} + + + } + data-test-id="spike-protection-discover-button" + onClick={() => + trackSpendVisibilityAnaltyics(SpendVisibilityEvents.SP_DISCOVER_CLICKED, { + organization, + subscription: subscription ?? undefined, + view: 'project_stats', + }) + } + to={{ + pathname: makeDiscoverPathname({ + organization, + path: '/homepage/', + }), + query: { + project: [project.id], + start: decodeScalar(spike.start), + end: decodeScalar(spike.end), + }, + }} + > + {getDiscoverDeprecation(organization) + ? t('Open in Explore') + : t('Open in Discover')} + + + + ); +} + +function SpikeHistoryContent({ + dataCategoryInfo, + onEnableSpikeProtection, + project, + spikes, + isLoading, +}: Props) { + const organization = useOrganization(); - renderEmptyMessage() { - const {organization} = this.props; + if (isLoading) { return ( - - {t('No Significant Spikes')} -

- {t( - 'Spike Protection is enabled for this project, but there are no significant spikes that lasted 2hrs or longer.' - )} -
- {tct('Please see the [auditLogLink: audit log] for all detected spikes.', { - auditLogLink: , - })} -

-
+ + + ); } - renderDisabledMessage() { - const {project, subscription, onEnableSpikeProtection} = this.props; + if (!isSpikeProtectionEnabled(project)) { return ( {t('Spike Protection Disabled')} @@ -224,7 +224,6 @@ class SpikeProtectionHistoryTable extends Component {
@@ -232,80 +231,75 @@ class SpikeProtectionHistoryTable extends Component { ); } - renderTable() { - const {spikes, project, isLoading} = this.props; - - if (isLoading ?? false) { - return ( - - - - ); - } - - if (!isSpikeProtectionEnabled(project)) { - return this.renderDisabledMessage(); - } - - if (spikes.length === 0) { - return this.renderEmptyMessage(); - } - + if (spikes.length === 0) { return ( - - {this.headers.map((header, i) => ( - {header} - ))} - - } - > - {spikes.map(spike => this.renderSpikeRow(spike))} - + + {t('No Significant Spikes')} +

+ {t( + 'Spike Protection is enabled for this project, but there are no significant spikes that lasted 2hrs or longer.' + )} +
+ {tct('Please see the [auditLogLink: audit log] for all detected spikes.', { + auditLogLink: , + })} +

+
); } - render() { - const {organization} = this.props; - return ( -
- - - {t('Spike Protection')} - <PageHeadingQuestionTooltip - docsUrl={SPIKE_PROTECTION_DOCS_LINK} - title={t( - 'Sentry applies a dynamic rate limit to your account designed to protect you from short-term spikes.' - )} - /> - - } - to={`/settings/${organization.slug}/spike-protection/`} - > - {t('Spike Protection Settings')} - - - {this.renderTable()} -
- ); - } + return ( + + {HEADERS.map((header, i) => ( + {header} + ))} + + } + > + {spikes.map(spike => ( + + ))} + + ); } -export default withSubscription(withOrganization(SpikeProtectionHistoryTable)); +export function SpikeProtectionHistoryTable(props: Props) { + const organization = useOrganization(); -const Title = styled('div')` - font-weight: bold; - font-size: ${p => p.theme.font.size.lg}; - color: ${p => p.theme.colors.gray500}; - display: flex; - flex: 1; - align-items: center; - gap: ${p => p.theme.space.sm}; -`; + return ( +
+ + + + {t('Spike Protection')} + + + + } + to={`/settings/${organization.slug}/spike-protection/`} + > + {t('Spike Protection Settings')} + + + +
+ ); +} const EmptySpikeHistory = styled(Panel)` width: 100%;