123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- import {Fragment} from 'react';
- import round from 'lodash/round';
- import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
- import Count from 'sentry/components/count';
- import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import ScoreCard from 'sentry/components/scoreCard';
- import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
- import {IconArrow} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import type {Organization, PageFilters} from 'sentry/types';
- import {defined} from 'sentry/utils';
- import type {TableData} from 'sentry/utils/discover/discoverQuery';
- import {getPeriod} from 'sentry/utils/duration/getPeriod';
- import {getTermHelp, PerformanceTerm} from 'sentry/views/performance/data';
- import MissingPerformanceButtons from '../missingFeatureButtons/missingPerformanceButtons';
- type Props = DeprecatedAsyncComponent['props'] & {
- isProjectStabilized: boolean;
- organization: Organization;
- selection: PageFilters;
- hasTransactions?: boolean;
- query?: string;
- };
- type State = DeprecatedAsyncComponent['state'] & {
- currentApdex: TableData | null;
- previousApdex: TableData | null;
- };
- class ProjectApdexScoreCard extends DeprecatedAsyncComponent<Props, State> {
- shouldRenderBadRequests = true;
- getDefaultState() {
- return {
- ...super.getDefaultState(),
- currentApdex: null,
- previousApdex: null,
- };
- }
- getEndpoints() {
- const {organization, selection, isProjectStabilized, hasTransactions, query} =
- this.props;
- if (!this.hasFeature() || !isProjectStabilized || !hasTransactions) {
- return [];
- }
- const {projects, environments, datetime} = selection;
- const {period} = datetime;
- const commonQuery = {
- environment: environments,
- project: projects.map(proj => String(proj)),
- field: ['apdex()'],
- query: ['event.type:transaction count():>0', query].join(' ').trim(),
- };
- const endpoints: ReturnType<DeprecatedAsyncComponent['getEndpoints']> = [
- [
- 'currentApdex',
- `/organizations/${organization.slug}/events/`,
- {query: {...commonQuery, ...normalizeDateTimeParams(datetime)}},
- ],
- ];
- if (
- shouldFetchPreviousPeriod({
- start: datetime.start,
- end: datetime.end,
- period: datetime.period,
- })
- ) {
- const {start: previousStart} = parseStatsPeriod(
- getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
- .statsPeriod!
- );
- const {start: previousEnd} = parseStatsPeriod(
- getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
- .statsPeriod!
- );
- endpoints.push([
- 'previousApdex',
- `/organizations/${organization.slug}/events/`,
- {query: {...commonQuery, start: previousStart, end: previousEnd}},
- ]);
- }
- return endpoints;
- }
- componentDidUpdate(prevProps: Props) {
- const {selection, isProjectStabilized, hasTransactions, query} = this.props;
- if (
- prevProps.selection !== selection ||
- prevProps.hasTransactions !== hasTransactions ||
- prevProps.isProjectStabilized !== isProjectStabilized ||
- prevProps.query !== query
- ) {
- this.remountComponent();
- }
- }
- hasFeature() {
- return this.props.organization.features.includes('performance-view');
- }
- get cardTitle() {
- return t('Apdex');
- }
- get cardHelp() {
- const {organization} = this.props;
- const baseHelp = getTermHelp(organization, PerformanceTerm.APDEX);
- if (this.trend) {
- return baseHelp + t(' This shows how it has changed since the last period.');
- }
- return baseHelp;
- }
- get currentApdex() {
- const {currentApdex} = this.state;
- const apdex = currentApdex?.data[0]?.['apdex()'];
- return typeof apdex === 'undefined' ? undefined : Number(apdex);
- }
- get previousApdex() {
- const {previousApdex} = this.state;
- const apdex = previousApdex?.data[0]?.['apdex()'];
- return typeof apdex === 'undefined' ? undefined : Number(apdex);
- }
- get trend() {
- if (this.currentApdex && this.previousApdex) {
- return round(this.currentApdex - this.previousApdex, 3);
- }
- return null;
- }
- get trendStatus(): React.ComponentProps<typeof ScoreCard>['trendStatus'] {
- if (!this.trend) {
- return undefined;
- }
- return this.trend > 0 ? 'good' : 'bad';
- }
- renderLoading() {
- return this.renderBody();
- }
- renderMissingFeatureCard() {
- const {organization} = this.props;
- return (
- <ScoreCard
- title={this.cardTitle}
- help={this.cardHelp}
- score={<MissingPerformanceButtons organization={organization} />}
- />
- );
- }
- renderScore() {
- return defined(this.currentApdex) ? <Count value={this.currentApdex} /> : '\u2014';
- }
- renderTrend() {
- // we want to show trend only after currentApdex has loaded to prevent jumping
- return defined(this.currentApdex) && defined(this.trend) ? (
- <Fragment>
- {this.trend >= 0 ? (
- <IconArrow direction="up" size="xs" />
- ) : (
- <IconArrow direction="down" size="xs" />
- )}
- <Count value={Math.abs(this.trend)} />
- </Fragment>
- ) : null;
- }
- renderBody() {
- const {hasTransactions} = this.props;
- if (!this.hasFeature() || hasTransactions === false) {
- return this.renderMissingFeatureCard();
- }
- return (
- <ScoreCard
- title={this.cardTitle}
- help={this.cardHelp}
- score={this.renderScore()}
- trend={this.renderTrend()}
- trendStatus={this.trendStatus}
- />
- );
- }
- }
- export default ProjectApdexScoreCard;
|