Skip to content

Commit

Permalink
ref(app-starts): Generalize screen table component (#69839)
Browse files Browse the repository at this point in the history
This component can be reused in the other mobile modules, generalize the
screen table so we can pass in custom header name maps and body cell
renderers.
  • Loading branch information
narsaynorath committed Apr 29, 2024
1 parent 9fb3a8b commit 0d46223
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import useRouter from 'sentry/utils/useRouter';
import {prepareQueryForLandingPage} from 'sentry/views/performance/data';
import {AverageComparisonChart} from 'sentry/views/performance/mobile/appStarts/screens/averageComparisonChart';
import {CountChart} from 'sentry/views/performance/mobile/appStarts/screens/countChart';
import {ScreensTable} from 'sentry/views/performance/mobile/appStarts/screens/screensTable';
import {AppStartScreens} from 'sentry/views/performance/mobile/appStarts/screens/screensTable';
import {COLD_START_TYPE} from 'sentry/views/performance/mobile/appStarts/screenSummary/startTypeSelector';
import {
getFreeTextFromQuery,
Expand Down Expand Up @@ -237,7 +237,7 @@ function AppStartup({additionalFilters, chartHeight}: Props) {
)
}
/>
<ScreensTable
<AppStartScreens
eventView={tableEventView}
data={topTransactionsData}
isLoading={topTransactionsLoading}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {render, screen} from 'sentry-test/reactTestingLibrary';

import EventView from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {AppStartScreens} from 'sentry/views/performance/mobile/appStarts/screens/screensTable';
import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';

jest.mock('sentry/views/starfish/queries/useReleases');

jest.mocked(useReleaseSelection).mockReturnValue({
primaryRelease: 'com.example.vu.android@2.10.5',
isLoading: false,
secondaryRelease: 'com.example.vu.android@2.10.3+42',
});

function getMockEventView({fields}) {
return new EventView({
id: '1',
name: 'mock query',
fields,

sorts: [],
query: '',
project: [],
start: '2019-10-01T00:00:00',
end: '2019-10-02T00:00:00',
statsPeriod: '14d',
environment: [],
additionalConditions: new MutableSearch(''),
createdBy: undefined,
interval: undefined,
display: '',
team: [],
topEvents: undefined,
yAxis: undefined,
});
}

describe('AppStartScreens', () => {
it('renders the correct headers', () => {
render(
<AppStartScreens
data={{
data: [],
meta: {
fields: [],
},
}}
eventView={getMockEventView({fields: []})}
isLoading={false}
pageLinks={undefined}
/>
);

expect(screen.getByRole('columnheader', {name: 'Screen'})).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Cold Start (R1)'})
).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Cold Start (R2)'})
).toBeInTheDocument();
expect(screen.getByRole('columnheader', {name: 'Change'})).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {name: 'Type Breakdown'})
).toBeInTheDocument();
expect(screen.getByRole('columnheader', {name: 'Count'})).toBeInTheDocument();
});

it('renders custom transaction and breakdown fields', () => {
render(
<AppStartScreens
data={{
data: [
{
id: '1',
transaction: 'Screen 1',
'avg_if(measurements.app_start_cold,release,com.example.vu.android@2.10.5)': 100,
'avg_if(measurements.app_start_cold,release,com.example.vu.android@2.10.3+42)': 200,
'avg_compare(measurements.app_start_cold,release,com.example.vu.android@2.10.5,com.example.vu.android@2.10.3+42)': 50,
app_start_breakdown: 'breakdown',
'count_starts(measurements.app_start_cold)': 10,
},
],
meta: {
fields: [],
},
}}
eventView={getMockEventView({fields: []})}
isLoading={false}
pageLinks={undefined}
/>
);

expect(screen.getByRole('link', {name: 'Screen 1'})).toBeInTheDocument();
expect(screen.getByTestId('app-start-breakdown')).toBeInTheDocument();
});
});
120 changes: 29 additions & 91 deletions static/app/views/performance/mobile/appStarts/screens/screensTable.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import {Fragment} from 'react';
import * as qs from 'query-string';

import type {GridColumnHeader} from 'sentry/components/gridEditable';
import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
import SortLink from 'sentry/components/gridEditable/sortLink';
import Link from 'sentry/components/links/link';
import Pagination from 'sentry/components/pagination';
import {t} from 'sentry/locale';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import type {MetaType} from 'sentry/utils/discover/eventView';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
import type EventView from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {fieldAlignment} from 'sentry/utils/discover/fields';
import {decodeScalar} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
import Breakdown from 'sentry/views/performance/mobile/appStarts/screens/breakdown';
import {COLD_START_TYPE} from 'sentry/views/performance/mobile/appStarts/screenSummary/startTypeSelector';
import {ScreensTable} from 'sentry/views/performance/mobile/components/screensTable';
import {TOP_SCREENS} from 'sentry/views/performance/mobile/screenload/screens';
import {COLD_START_COLOR, WARM_START_COLOR} from 'sentry/views/starfish/colors';
import {
Expand All @@ -36,7 +29,7 @@ type Props = {
pageLinks: string | undefined;
};

export function ScreensTable({data, eventView, isLoading, pageLinks}: Props) {
export function AppStartScreens({data, eventView, isLoading, pageLinks}: Props) {
const location = useLocation();
const organization = useOrganization();
const {primaryRelease, secondaryRelease} = useReleaseSelection();
Expand Down Expand Up @@ -72,8 +65,8 @@ export function ScreensTable({data, eventView, isLoading, pageLinks}: Props) {
};

function renderBodyCell(column, row): React.ReactNode {
if (!data?.meta || !data?.meta.fields) {
return row[column.key];
if (!data) {
return null;
}

const index = data.data.indexOf(row);
Expand Down Expand Up @@ -107,6 +100,7 @@ export function ScreensTable({data, eventView, isLoading, pageLinks}: Props) {
if (field === 'app_start_breakdown') {
return (
<Breakdown
data-test-id="app-start-breakdown"
row={row}
breakdownGroups={[
{
Expand All @@ -124,87 +118,31 @@ export function ScreensTable({data, eventView, isLoading, pageLinks}: Props) {
);
}

const renderer = getFieldRenderer(column.key, data?.meta.fields, false);
return renderer(row, {
location,
organization,
unit: data?.meta.units?.[column.key],
});
}

function renderHeadCell(
column: GridColumnHeader,
tableMeta?: MetaType
): React.ReactNode {
const fieldType = tableMeta?.fields?.[column.key];
const alignment = fieldAlignment(column.key as string, fieldType);
const field = {
field: column.key as string,
width: column.width,
};

function generateSortLink() {
if (!tableMeta) {
return undefined;
}

const nextEventView = eventView.sortOnField(field, tableMeta);
const queryStringObject = nextEventView.generateQueryStringObject();

return {
...location,
query: {...location.query, sort: queryStringObject.sort},
};
}

const currentSort = eventView.sortForField(field, tableMeta);
const currentSortKind = currentSort ? currentSort.kind : undefined;
const canSort = isFieldSortable(field, tableMeta);

const sortLink = (
<SortLink
align={alignment}
title={column.name}
direction={currentSortKind}
canSort={canSort}
generateSortLink={generateSortLink}
/>
);
return sortLink;
return null;
}

return (
<Fragment>
<GridEditable
isLoading={isLoading}
data={data?.data as TableDataRow[]}
columnOrder={[
'transaction',
`avg_if(measurements.app_start_${startType},release,${primaryRelease})`,
`avg_if(measurements.app_start_${startType},release,${secondaryRelease})`,
`avg_compare(measurements.app_start_${startType},release,${primaryRelease},${secondaryRelease})`,
'app_start_breakdown',
`count_starts(measurements.app_start_${startType})`,
].map(columnKey => {
return {
key: columnKey,
name: columnNameMap[columnKey],
width: COL_WIDTH_UNDEFINED,
};
})}
columnSortBy={[
{
key: `count_starts_measurements_app_start_${startType}`,
order: 'desc',
},
]}
location={location}
grid={{
renderHeadCell: column => renderHeadCell(column, data?.meta),
renderBodyCell,
}}
/>
<Pagination pageLinks={pageLinks} />
</Fragment>
<ScreensTable
columnNameMap={columnNameMap}
data={data}
eventView={eventView}
isLoading={isLoading}
pageLinks={pageLinks}
columnOrder={[
'transaction',
`avg_if(measurements.app_start_${startType},release,${primaryRelease})`,
`avg_if(measurements.app_start_${startType},release,${secondaryRelease})`,
`avg_compare(measurements.app_start_${startType},release,${primaryRelease},${secondaryRelease})`,
'app_start_breakdown',
`count_starts(measurements.app_start_${startType})`,
]}
defaultSort={[
{
key: `count_starts_measurements_app_start_${startType}`,
order: 'desc',
},
]}
customBodyCellRenderer={renderBodyCell}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {render, screen} from 'sentry-test/reactTestingLibrary';

import EventView from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {ScreensTable} from 'sentry/views/performance/mobile/components/screensTable';

function getMockEventView({fields}) {
return new EventView({
id: '1',
name: 'mock query',
fields,

sorts: [],
query: '',
project: [],
start: '2019-10-01T00:00:00',
end: '2019-10-02T00:00:00',
statsPeriod: '14d',
environment: [],
additionalConditions: new MutableSearch(''),
createdBy: undefined,
interval: undefined,
display: '',
team: [],
topEvents: undefined,
yAxis: undefined,
});
}

describe('ScreensTable', () => {
it('renders table header cells with translated names', () => {
render(
<ScreensTable
columnNameMap={{
transaction: 'Screen',
}}
columnOrder={['transaction']}
data={{
data: [{id: '1', transaction: 'Screen 1'}],
meta: {},
}}
defaultSort={[]}
eventView={getMockEventView({fields: [{field: 'transaction'}]})}
isLoading={false}
pageLinks={undefined}
/>
);

expect(screen.getByText('Screen')).toBeInTheDocument();
expect(screen.queryByText('transaction')).not.toBeInTheDocument();
});

it('renders body cells with custom renderer if applicable', () => {
render(
<ScreensTable
columnNameMap={{
transaction: 'Screen',
}}
columnOrder={['transaction', 'non-custom']}
data={{
data: [
{id: '1', transaction: 'Screen 1', 'non-custom': 'non customized value'},
],
meta: {fields: {transaction: 'string'}},
}}
defaultSort={[]}
eventView={getMockEventView({
fields: [{field: 'transaction'}, {field: 'non-custom'}],
})}
isLoading={false}
pageLinks={undefined}
customBodyCellRenderer={(column, row) => {
if (column.key === 'transaction') {
return `Custom rendered ${row.transaction}`;
}

return null;
}}
/>
);

expect(screen.getByText('Custom rendered Screen 1')).toBeInTheDocument();
expect(screen.getByText('non customized value')).toBeInTheDocument();
});
});

0 comments on commit 0d46223

Please sign in to comment.