mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
822 lines
25 KiB
TypeScript
822 lines
25 KiB
TypeScript
import {Fragment} from 'react';
|
|
import {css} from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import type {Location} from 'history';
|
|
|
|
import {Alert} from 'sentry/components/alert';
|
|
import Link from 'sentry/components/links/link';
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import NotAvailable from 'sentry/components/notAvailable';
|
|
import PanelItem from 'sentry/components/panels/panelItem';
|
|
import PanelTable from 'sentry/components/panels/panelTable';
|
|
import {IconArrow} from 'sentry/icons';
|
|
import {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, ReleaseProject} from 'sentry/types';
|
|
import type {TableData} from 'sentry/utils/discover/discoverQuery';
|
|
import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
|
|
import type EventView from 'sentry/utils/discover/eventView';
|
|
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
|
|
import {MobileVital, WebVital} from 'sentry/utils/fields';
|
|
import {
|
|
MOBILE_VITAL_DETAILS,
|
|
WEB_VITAL_DETAILS,
|
|
} from 'sentry/utils/performance/vitals/constants';
|
|
import {ProjectPerformanceType} from 'sentry/views/performance/utils';
|
|
|
|
type PerformanceCardTableProps = {
|
|
allReleasesEventView: EventView;
|
|
allReleasesTableData: TableData | null;
|
|
isLoading: boolean;
|
|
location: Location;
|
|
organization: Organization;
|
|
performanceType: string;
|
|
project: ReleaseProject;
|
|
releaseEventView: EventView;
|
|
thisReleaseTableData: TableData | null;
|
|
};
|
|
|
|
function PerformanceCardTable({
|
|
organization,
|
|
location,
|
|
project,
|
|
releaseEventView,
|
|
allReleasesTableData,
|
|
thisReleaseTableData,
|
|
performanceType,
|
|
isLoading,
|
|
}: PerformanceCardTableProps) {
|
|
const miseryRenderer =
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer('user_misery()', allReleasesTableData.meta, false);
|
|
|
|
function renderChange(
|
|
allReleasesScore: number,
|
|
thisReleaseScore: number,
|
|
meta: string
|
|
) {
|
|
if (allReleasesScore === undefined || thisReleaseScore === undefined) {
|
|
return <StyledNotAvailable />;
|
|
}
|
|
|
|
const trend = allReleasesScore - thisReleaseScore;
|
|
const trendSeconds = trend >= 1000 ? trend / 1000 : trend;
|
|
const trendPercentage = (allReleasesScore - thisReleaseScore) * 100;
|
|
const valPercentage = Math.round(Math.abs(trendPercentage));
|
|
const val = Math.abs(trendSeconds).toFixed(2);
|
|
|
|
if (trend === 0) {
|
|
return <SubText>{`0${meta === 'duration' ? 'ms' : '%'}`}</SubText>;
|
|
}
|
|
|
|
return (
|
|
<TrendText color={trend >= 0 ? 'success' : 'error'}>
|
|
{`${meta === 'duration' ? val : valPercentage}${
|
|
meta === 'duration' ? (trend >= 1000 ? 's' : 'ms') : '%'
|
|
}`}
|
|
<StyledIconArrow
|
|
color={trend >= 0 ? 'success' : 'error'}
|
|
direction={trend >= 0 ? 'down' : 'up'}
|
|
size="xs"
|
|
/>
|
|
</TrendText>
|
|
);
|
|
}
|
|
|
|
function userMiseryTrend() {
|
|
const allReleasesUserMisery = allReleasesTableData?.data?.[0]?.['user_misery()'];
|
|
const thisReleaseUserMisery = thisReleaseTableData?.data?.[0]?.['user_misery()'];
|
|
return (
|
|
<StyledPanelItem>
|
|
{renderChange(
|
|
allReleasesUserMisery as number,
|
|
thisReleaseUserMisery as number,
|
|
'number' as string
|
|
)}
|
|
</StyledPanelItem>
|
|
);
|
|
}
|
|
|
|
function renderFrontendPerformance() {
|
|
const webVitals = [
|
|
{title: WebVital.FCP, field: 'p75(measurements.fcp)'},
|
|
{title: WebVital.FID, field: 'p75(measurements.fid)'},
|
|
{title: WebVital.LCP, field: 'p75(measurements.lcp)'},
|
|
{title: WebVital.CLS, field: 'p75(measurements.cls)'},
|
|
];
|
|
|
|
const spans = [
|
|
{title: 'HTTP', column: 'p75(spans.http)', field: 'p75(spans.http)'},
|
|
{title: 'Browser', column: 'p75(spans.browser)', field: 'p75(spans.browser)'},
|
|
{title: 'Resource', column: 'p75(spans.resource)', field: 'p75(spans.resource)'},
|
|
];
|
|
|
|
const webVitalTitles = webVitals.map((vital, idx) => {
|
|
const newView = releaseEventView.withColumns([
|
|
{kind: 'field', field: `p75(${vital.title})`},
|
|
]);
|
|
return (
|
|
<SubTitle key={idx}>
|
|
<Link to={newView.getResultsViewUrlTarget(organization.slug)}>
|
|
{WEB_VITAL_DETAILS[vital.title].name} (
|
|
{WEB_VITAL_DETAILS[vital.title].acronym})
|
|
</Link>
|
|
</SubTitle>
|
|
);
|
|
});
|
|
|
|
const spanTitles = spans.map((span, idx) => {
|
|
const newView = releaseEventView.withColumns([
|
|
{kind: 'field', field: `${span.column}`},
|
|
]);
|
|
return (
|
|
<SubTitle key={idx}>
|
|
<Link to={newView.getResultsViewUrlTarget(organization.slug)}>
|
|
{span.title}
|
|
</Link>
|
|
</SubTitle>
|
|
);
|
|
});
|
|
|
|
const webVitalsRenderer = webVitals.map(
|
|
vital =>
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer(vital.field, allReleasesTableData?.meta, false)
|
|
);
|
|
|
|
const spansRenderer = spans.map(
|
|
span =>
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer(span.field, allReleasesTableData?.meta, false)
|
|
);
|
|
|
|
const webReleaseTrend = webVitals.map(vital => {
|
|
return {
|
|
allReleasesRow: {
|
|
data: allReleasesTableData?.data?.[0]?.[vital.field],
|
|
meta: allReleasesTableData?.meta?.[vital.field],
|
|
},
|
|
thisReleaseRow: {
|
|
data: thisReleaseTableData?.data?.[0]?.[vital.field],
|
|
meta: thisReleaseTableData?.meta?.[vital.field],
|
|
},
|
|
};
|
|
});
|
|
const spansReleaseTrend = spans.map(span => {
|
|
return {
|
|
allReleasesRow: {
|
|
data: allReleasesTableData?.data?.[0]?.[span.field],
|
|
meta: allReleasesTableData?.meta?.[span.field],
|
|
},
|
|
thisReleaseRow: {
|
|
data: thisReleaseTableData?.data?.[0]?.[span.field],
|
|
meta: thisReleaseTableData?.meta?.[span.field],
|
|
},
|
|
};
|
|
});
|
|
|
|
const emptyColumn = (
|
|
<div>
|
|
<SingleEmptySubText>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{webVitals.map((vital, index) => (
|
|
<MultipleEmptySubText key={vital[index]}>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</MultipleEmptySubText>
|
|
))}
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{spans.map((span, index) => (
|
|
<MultipleEmptySubText key={span[index]}>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</MultipleEmptySubText>
|
|
))}
|
|
</StyledPanelItem>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
<div>
|
|
<PanelItem>{t('User Misery')}</PanelItem>
|
|
<StyledPanelItem>
|
|
<div>{t('Web Vitals')}</div>
|
|
{webVitalTitles}
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<div>{t('Span Operations')}</div>
|
|
{spanTitles}
|
|
</StyledPanelItem>
|
|
</div>
|
|
{allReleasesTableData?.data.length === 0
|
|
? emptyColumn
|
|
: allReleasesTableData?.data.map((dataRow, idx) => {
|
|
const allReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const allReleasesWebVitals = webVitalsRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
const allReleasesSpans = spansRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{allReleasesWebVitals.map(webVital => webVital)}
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{allReleasesSpans.map(span => span)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
);
|
|
})}
|
|
{thisReleaseTableData?.data.length === 0
|
|
? emptyColumn
|
|
: thisReleaseTableData?.data.map((dataRow, idx) => {
|
|
const thisReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const thisReleasesWebVitals = webVitalsRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
const thisReleasesSpans = spansRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<div>
|
|
<UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{thisReleasesWebVitals.map(webVital => webVital)}
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{thisReleasesSpans.map(span => span)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
<div>
|
|
{userMiseryTrend()}
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{webReleaseTrend?.map(row =>
|
|
renderChange(
|
|
row.allReleasesRow?.data as number,
|
|
row.thisReleaseRow?.data as number,
|
|
row.allReleasesRow?.meta as string
|
|
)
|
|
)}
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{spansReleaseTrend?.map(row =>
|
|
renderChange(
|
|
row.allReleasesRow?.data as number,
|
|
row.thisReleaseRow?.data as number,
|
|
row.allReleasesRow?.meta as string
|
|
)
|
|
)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
function renderBackendPerformance() {
|
|
const spans = [
|
|
{title: 'HTTP', column: 'p75(spans.http)', field: 'p75_spans_http'},
|
|
{title: 'DB', column: 'p75(spans.db)', field: 'p75_spans_db'},
|
|
];
|
|
|
|
const spanTitles = spans.map((span, idx) => {
|
|
const newView = releaseEventView.withColumns([
|
|
{kind: 'field', field: `${span.column}`},
|
|
]);
|
|
return (
|
|
<SubTitle key={idx}>
|
|
<Link to={newView.getResultsViewUrlTarget(organization.slug)}>
|
|
{span.title}
|
|
</Link>
|
|
</SubTitle>
|
|
);
|
|
});
|
|
|
|
const apdexRenderer =
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer('apdex', allReleasesTableData.meta, false);
|
|
|
|
const spansRenderer = spans.map(
|
|
span =>
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer(span.field, allReleasesTableData?.meta, false)
|
|
);
|
|
|
|
const spansReleaseTrend = spans.map(span => {
|
|
return {
|
|
allReleasesRow: {
|
|
data: allReleasesTableData?.data?.[0]?.[span.field],
|
|
meta: allReleasesTableData?.meta?.[span.field],
|
|
},
|
|
thisReleaseRow: {
|
|
data: thisReleaseTableData?.data?.[0]?.[span.field],
|
|
meta: thisReleaseTableData?.meta?.[span.field],
|
|
},
|
|
};
|
|
});
|
|
|
|
function apdexTrend() {
|
|
const allReleasesApdex = allReleasesTableData?.data?.[0]?.apdex;
|
|
const thisReleaseApdex = thisReleaseTableData?.data?.[0]?.apdex;
|
|
return (
|
|
<StyledPanelItem>
|
|
{renderChange(
|
|
allReleasesApdex as number,
|
|
thisReleaseApdex as number,
|
|
'string' as string
|
|
)}
|
|
</StyledPanelItem>
|
|
);
|
|
}
|
|
|
|
const emptyColumn = (
|
|
<div>
|
|
<SingleEmptySubText>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
<SingleEmptySubText>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{spans.map((span, index) => (
|
|
<MultipleEmptySubText key={span[index]}>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</MultipleEmptySubText>
|
|
))}
|
|
</StyledPanelItem>
|
|
</div>
|
|
);
|
|
return (
|
|
<Fragment>
|
|
<div>
|
|
<PanelItem>{t('User Misery')}</PanelItem>
|
|
<StyledPanelItem>
|
|
<div>{t('Apdex')}</div>
|
|
</StyledPanelItem>
|
|
<StyledPanelItem>
|
|
<div>{t('Span Operations')}</div>
|
|
{spanTitles}
|
|
</StyledPanelItem>
|
|
</div>
|
|
{allReleasesTableData?.data.length === 0
|
|
? emptyColumn
|
|
: allReleasesTableData?.data.map((dataRow, idx) => {
|
|
const allReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const allReleasesApdex = apdexRenderer?.(dataRow, {organization, location});
|
|
const allReleasesSpans = spansRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
|
|
<ApdexPanelItem>{allReleasesApdex}</ApdexPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{allReleasesSpans.map(span => span)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
);
|
|
})}
|
|
{thisReleaseTableData?.data.length === 0
|
|
? emptyColumn
|
|
: thisReleaseTableData?.data.map((dataRow, idx) => {
|
|
const thisReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const thisReleasesApdex = apdexRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const thisReleasesSpans = spansRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
|
|
<ApdexPanelItem>{thisReleasesApdex}</ApdexPanelItem>
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{thisReleasesSpans.map(span => span)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
);
|
|
})}
|
|
<div>
|
|
{userMiseryTrend()}
|
|
{apdexTrend()}
|
|
<StyledPanelItem>
|
|
<TitleSpace />
|
|
{spansReleaseTrend?.map(row =>
|
|
renderChange(
|
|
row.allReleasesRow?.data as number,
|
|
row.thisReleaseRow?.data as number,
|
|
row.allReleasesRow?.meta as string
|
|
)
|
|
)}
|
|
</StyledPanelItem>
|
|
</div>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
function renderMobilePerformance() {
|
|
const mobileVitals = [
|
|
MobileVital.APP_START_COLD,
|
|
MobileVital.APP_START_WARM,
|
|
MobileVital.FRAMES_SLOW,
|
|
MobileVital.FRAMES_FROZEN,
|
|
];
|
|
const mobileVitalTitles = mobileVitals.map(mobileVital => {
|
|
return (
|
|
<PanelItem key={mobileVital}>{MOBILE_VITAL_DETAILS[mobileVital].name}</PanelItem>
|
|
);
|
|
});
|
|
|
|
const mobileVitalFields = [
|
|
'p75(measurements.app_start_cold)',
|
|
'p75(measurements.app_start_warm)',
|
|
'p75(measurements.frames_slow)',
|
|
'p75(measurements.frames_frozen)',
|
|
];
|
|
const mobileVitalsRenderer = mobileVitalFields.map(
|
|
field =>
|
|
allReleasesTableData?.meta &&
|
|
getFieldRenderer(field, allReleasesTableData?.meta, false)
|
|
);
|
|
|
|
const mobileReleaseTrend = mobileVitalFields.map(field => {
|
|
return {
|
|
allReleasesRow: {
|
|
data: allReleasesTableData?.data?.[0]?.[field],
|
|
meta: allReleasesTableData?.meta?.[field],
|
|
},
|
|
thisReleaseRow: {
|
|
data: thisReleaseTableData?.data?.[0]?.[field],
|
|
meta: thisReleaseTableData?.meta?.[field],
|
|
},
|
|
};
|
|
});
|
|
|
|
const emptyColumn = (
|
|
<div>
|
|
<SingleEmptySubText>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
{mobileVitalFields.map((vital, index) => (
|
|
<SingleEmptySubText key={vital[index]}>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
<div>
|
|
<PanelItem>{t('User Misery')}</PanelItem>
|
|
{mobileVitalTitles}
|
|
</div>
|
|
{allReleasesTableData?.data.length === 0
|
|
? emptyColumn
|
|
: allReleasesTableData?.data.map((dataRow, idx) => {
|
|
const allReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const allReleasesMobile = mobileVitalsRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
|
|
{allReleasesMobile.map((mobileVital, i) => (
|
|
<StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
{thisReleaseTableData?.data.length === 0
|
|
? emptyColumn
|
|
: thisReleaseTableData?.data.map((dataRow, idx) => {
|
|
const thisReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
const thisReleasesMobile = mobileVitalsRenderer?.map(renderer =>
|
|
renderer?.(dataRow, {organization, location})
|
|
);
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
|
|
{thisReleasesMobile.map((mobileVital, i) => (
|
|
<StyledPanelItem key={i}>{mobileVital}</StyledPanelItem>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
<div>
|
|
{userMiseryTrend()}
|
|
{mobileReleaseTrend?.map((row, idx) => (
|
|
<StyledPanelItem key={idx}>
|
|
{renderChange(
|
|
row.allReleasesRow?.data as number,
|
|
row.thisReleaseRow?.data as number,
|
|
row.allReleasesRow?.meta as string
|
|
)}
|
|
</StyledPanelItem>
|
|
))}
|
|
</div>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
function renderUnknownPerformance() {
|
|
const emptyColumn = (
|
|
<div>
|
|
<SingleEmptySubText>
|
|
<StyledNotAvailable tooltip={t('No results found')} />
|
|
</SingleEmptySubText>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
<div>
|
|
<PanelItem>{t('User Misery')}</PanelItem>
|
|
</div>
|
|
{allReleasesTableData?.data.length === 0
|
|
? emptyColumn
|
|
: allReleasesTableData?.data.map((dataRow, idx) => {
|
|
const allReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{allReleasesMisery}</UserMiseryPanelItem>
|
|
</div>
|
|
);
|
|
})}
|
|
{thisReleaseTableData?.data.length === 0
|
|
? emptyColumn
|
|
: thisReleaseTableData?.data.map((dataRow, idx) => {
|
|
const thisReleasesMisery = miseryRenderer?.(dataRow, {
|
|
organization,
|
|
location,
|
|
});
|
|
|
|
return (
|
|
<div key={idx}>
|
|
<UserMiseryPanelItem>{thisReleasesMisery}</UserMiseryPanelItem>
|
|
</div>
|
|
);
|
|
})}
|
|
<div>{userMiseryTrend()}</div>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
const loader = <StyledLoadingIndicator />;
|
|
|
|
const platformPerformanceRender = {
|
|
[ProjectPerformanceType.FRONTEND]: {
|
|
title: t('Frontend Performance'),
|
|
section: renderFrontendPerformance(),
|
|
},
|
|
[ProjectPerformanceType.BACKEND]: {
|
|
title: t('Backend Performance'),
|
|
section: renderBackendPerformance(),
|
|
},
|
|
[ProjectPerformanceType.MOBILE]: {
|
|
title: t('Mobile Performance'),
|
|
section: renderMobilePerformance(),
|
|
},
|
|
[ProjectPerformanceType.ANY]: {
|
|
title: t('[Unknown] Performance'),
|
|
section: renderUnknownPerformance(),
|
|
},
|
|
};
|
|
|
|
const isUnknownPlatform = performanceType === ProjectPerformanceType.ANY;
|
|
|
|
return (
|
|
<Fragment>
|
|
<HeadCellContainer>
|
|
{platformPerformanceRender[performanceType].title}
|
|
</HeadCellContainer>
|
|
{isUnknownPlatform && (
|
|
<StyledAlert type="warning" showIcon system>
|
|
{tct(
|
|
'For more performance metrics, specify which platform this project is using in [link]',
|
|
{
|
|
link: (
|
|
<Link to={`/settings/${organization.slug}/projects/${project.slug}/`}>
|
|
{t('project settings.')}
|
|
</Link>
|
|
),
|
|
}
|
|
)}
|
|
</StyledAlert>
|
|
)}
|
|
<StyledPanelTable
|
|
isLoading={isLoading}
|
|
headers={[
|
|
<Cell key="description" align="left">
|
|
{t('Description')}
|
|
</Cell>,
|
|
<Cell key="releases" align="right">
|
|
{t('All Releases')}
|
|
</Cell>,
|
|
<Cell key="release" align="right">
|
|
{t('This Release')}
|
|
</Cell>,
|
|
<Cell key="change" align="right">
|
|
{t('Change')}
|
|
</Cell>,
|
|
]}
|
|
disablePadding
|
|
loader={loader}
|
|
disableTopBorder={isUnknownPlatform}
|
|
>
|
|
{platformPerformanceRender[performanceType].section}
|
|
</StyledPanelTable>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
interface Props {
|
|
allReleasesEventView: EventView;
|
|
location: Location;
|
|
organization: Organization;
|
|
performanceType: string;
|
|
project: ReleaseProject;
|
|
releaseEventView: EventView;
|
|
}
|
|
|
|
function PerformanceCardTableWrapper({
|
|
organization,
|
|
project,
|
|
allReleasesEventView,
|
|
releaseEventView,
|
|
performanceType,
|
|
location,
|
|
}: Props) {
|
|
return (
|
|
<DiscoverQuery
|
|
eventView={allReleasesEventView}
|
|
orgSlug={organization.slug}
|
|
location={location}
|
|
>
|
|
{({isLoading, tableData: allReleasesTableData}) => (
|
|
<DiscoverQuery
|
|
eventView={releaseEventView}
|
|
orgSlug={organization.slug}
|
|
location={location}
|
|
>
|
|
{({isLoading: isReleaseLoading, tableData: thisReleaseTableData}) => (
|
|
<PerformanceCardTable
|
|
isLoading={isLoading || isReleaseLoading}
|
|
organization={organization}
|
|
location={location}
|
|
project={project}
|
|
allReleasesEventView={allReleasesEventView}
|
|
releaseEventView={releaseEventView}
|
|
allReleasesTableData={allReleasesTableData}
|
|
thisReleaseTableData={thisReleaseTableData}
|
|
performanceType={performanceType}
|
|
/>
|
|
)}
|
|
</DiscoverQuery>
|
|
)}
|
|
</DiscoverQuery>
|
|
);
|
|
}
|
|
|
|
export default PerformanceCardTableWrapper;
|
|
|
|
const emptyFieldCss = p => css`
|
|
color: ${p.theme.chartOther};
|
|
text-align: right;
|
|
`;
|
|
|
|
const StyledLoadingIndicator = styled(LoadingIndicator)`
|
|
margin: 70px auto;
|
|
`;
|
|
|
|
const HeadCellContainer = styled('div')`
|
|
font-size: ${p => p.theme.fontSizeExtraLarge};
|
|
padding: ${space(2)};
|
|
border-top: 1px solid ${p => p.theme.border};
|
|
border-left: 1px solid ${p => p.theme.border};
|
|
border-right: 1px solid ${p => p.theme.border};
|
|
border-top-left-radius: ${p => p.theme.borderRadius};
|
|
border-top-right-radius: ${p => p.theme.borderRadius};
|
|
`;
|
|
|
|
const StyledPanelTable = styled(PanelTable)<{disableTopBorder: boolean}>`
|
|
border-top-left-radius: 0;
|
|
border-top-right-radius: 0;
|
|
border-top: ${p => (p.disableTopBorder ? 'none' : `1px solid ${p.theme.border}`)};
|
|
@media (max-width: ${p => p.theme.breakpoints.large}) {
|
|
grid-template-columns: min-content 1fr 1fr 1fr;
|
|
}
|
|
`;
|
|
|
|
const StyledPanelItem = styled(PanelItem)`
|
|
display: block;
|
|
white-space: nowrap;
|
|
width: 100%;
|
|
`;
|
|
|
|
const SubTitle = styled('div')`
|
|
margin-left: ${space(3)};
|
|
`;
|
|
|
|
const TitleSpace = styled('div')`
|
|
height: 24px;
|
|
`;
|
|
|
|
const UserMiseryPanelItem = styled(PanelItem)`
|
|
justify-content: flex-end;
|
|
`;
|
|
|
|
const ApdexPanelItem = styled(PanelItem)`
|
|
text-align: right;
|
|
`;
|
|
|
|
const SingleEmptySubText = styled(PanelItem)`
|
|
display: block;
|
|
${emptyFieldCss}
|
|
`;
|
|
|
|
const MultipleEmptySubText = styled('div')`
|
|
${emptyFieldCss}
|
|
`;
|
|
|
|
const Cell = styled('div')<{align: 'left' | 'right'}>`
|
|
text-align: ${p => p.align};
|
|
margin-left: ${p => p.align === 'left' && space(2)};
|
|
padding-right: ${p => p.align === 'right' && space(2)};
|
|
${p => p.theme.overflowEllipsis}
|
|
`;
|
|
|
|
const StyledAlert = styled(Alert)`
|
|
border-top: 1px solid ${p => p.theme.border};
|
|
border-right: 1px solid ${p => p.theme.border};
|
|
border-left: 1px solid ${p => p.theme.border};
|
|
margin-bottom: 0;
|
|
`;
|
|
|
|
const StyledNotAvailable = styled(NotAvailable)`
|
|
text-align: right;
|
|
`;
|
|
|
|
const SubText = styled('div')`
|
|
color: ${p => p.theme.subText};
|
|
text-align: right;
|
|
`;
|
|
|
|
const TrendText = styled('div')<{color: string}>`
|
|
color: ${p => p.theme[p.color]};
|
|
text-align: right;
|
|
`;
|
|
|
|
const StyledIconArrow = styled(IconArrow)<{color: string}>`
|
|
color: ${p => p.theme[p.color]};
|
|
margin-left: ${space(0.5)};
|
|
`;
|