Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(browser-starfish): create basic image view #58785

Merged
merged 7 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Fragment, MouseEventHandler} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';

import SwitchButton from 'sentry/components/switchButton';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {useLocation} from 'sentry/utils/useLocation';
import ResourceTable from 'sentry/views/performance/browser/resources/imageView/resourceTable';
import {useImageResourceSort} from 'sentry/views/performance/browser/resources/imageView/utils/useImageResourceSort';
import {FilterOptionsContainer} from 'sentry/views/performance/browser/resources/jsCssView';
import {useResourceModuleFilters} from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
import {SpanIndexedField} from 'sentry/views/starfish/types';

const {RESOURCE_RENDER_BLOCKING_STATUS} = SpanIndexedField;

function ImageView() {
const sort = useImageResourceSort();
const location = useLocation();
const filters = useResourceModuleFilters();

const handleBlockingToggle: MouseEventHandler = () => {
const hasBlocking = filters[RESOURCE_RENDER_BLOCKING_STATUS] === 'blocking';
const newBlocking = hasBlocking ? undefined : 'blocking';
browserHistory.push({
...location,
query: {
...location.query,
[RESOURCE_RENDER_BLOCKING_STATUS]: newBlocking,
},
});
};

return (
<Fragment>
<FilterOptionsContainer>
<SwitchContainer>
<SwitchButton
toggle={handleBlockingToggle}
isActive={filters[RESOURCE_RENDER_BLOCKING_STATUS] === 'blocking'}
/>
{t('Render Blocking')}
</SwitchContainer>
</FilterOptionsContainer>
<ResourceTable sort={sort} />
</Fragment>
);
}

const SwitchContainer = styled('div')`
display: flex;
align-items: center;
column-gap: ${space(1)};
`;

export default ImageView;
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {Fragment} from 'react';
import {Link} from 'react-router';

import FileSize from 'sentry/components/fileSize';
import GridEditable, {
COL_WIDTH_UNDEFINED,
GridColumnHeader,
GridColumnOrder,
} from 'sentry/components/gridEditable';
import Pagination from 'sentry/components/pagination';
import {t} from 'sentry/locale';
import {useLocation} from 'sentry/utils/useLocation';
import {ValidSort} from 'sentry/views/performance/browser/resources/imageView/utils/useImageResourceSort';
import {useIndexedResourcesQuery} from 'sentry/views/performance/browser/resources/imageView/utils/useIndexedResourcesQuery';
import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
import {SpanMetricsField} from 'sentry/views/starfish/types';

const {SPAN_DESCRIPTION, SPAN_SELF_TIME, HTTP_RESPONSE_CONTENT_LENGTH} = SpanMetricsField;

type Row = {
'http.response_content_length': number;
id: string;
project: string;
'resource.render_blocking_status': '' | 'non-blocking' | 'blocking';
'span.description': string;
'span.self_time': number;
};

type Column = GridColumnHeader<keyof Row>;

type Props = {
sort: ValidSort;
};

function ResourceTable({sort}: Props) {
const location = useLocation();
const {data, isLoading, pageLinks} = useIndexedResourcesQuery();

const columnOrder: GridColumnOrder<keyof Row>[] = [
{key: SPAN_DESCRIPTION, width: COL_WIDTH_UNDEFINED, name: 'Resource name'},
{key: `${SPAN_SELF_TIME}`, width: COL_WIDTH_UNDEFINED, name: 'Duration'},
{
key: HTTP_RESPONSE_CONTENT_LENGTH,
width: COL_WIDTH_UNDEFINED,
name: t('Resource size'),
},
];
const tableData: Row[] = data.length
? data.map(span => ({
...span,
'http.decoded_response_content_length': Math.floor(
Math.random() * (1000 - 500) + 500
),
}))
: [];

const renderBodyCell = (col: Column, row: Row) => {
const {key} = col;
if (key === SPAN_DESCRIPTION) {
return (
<Link to={`/performance/${row.project}:${row['transaction.id']}#span-${row.id}`}>
{row[key]}
</Link>
);
}
if (key === 'http.response_content_length') {
return <FileSize bytes={row[key]} />;
}
if (key === `span.self_time`) {
return <DurationCell milliseconds={row[key]} />;
}
return <span>{row[key]}</span>;
};

return (
<Fragment>
<GridEditable
data={tableData}
isLoading={isLoading}
columnOrder={columnOrder}
columnSortBy={[
{
key: sort.field,
order: sort.kind,
},
]}
grid={{
renderHeadCell: column =>
renderHeadCell({
column,
location,
sort,
}),
renderBodyCell,
}}
location={location}
/>
<Pagination pageLinks={pageLinks} />
</Fragment>
);
}

export default ResourceTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {fromSorts} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {useLocation} from 'sentry/utils/useLocation';
import {SpanMetricsField} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';

const {SPAN_SELF_TIME, SPAN_DESCRIPTION, HTTP_RESPONSE_CONTENT_LENGTH} = SpanMetricsField;

type Query = {
sort?: string;
};

const SORTABLE_FIELDS = [
SPAN_SELF_TIME,
SPAN_DESCRIPTION,
HTTP_RESPONSE_CONTENT_LENGTH,
] as const;

export type ValidSort = Sort & {
field: (typeof SORTABLE_FIELDS)[number];
};

/**
* Parses a `Sort` object from the URL. In case of multiple specified sorts
* picks the first one, since span module UIs only support one sort at a time.
*/
export function useImageResourceSort(
sortParameterName: QueryParameterNames | 'sort' = 'sort',
fallback: Sort = DEFAULT_SORT
) {
const location = useLocation<Query>();

return fromSorts(location.query[sortParameterName]).filter(isAValidSort)[0] ?? fallback;
}

const DEFAULT_SORT: Sort = {
kind: 'desc',
field: SORTABLE_FIELDS[2],
};

function isAValidSort(sort: Sort): sort is ValidSort {
return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
import EventView from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import {useResourceModuleFilters} from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
import {SpanIndexedField} from 'sentry/views/starfish/types';

const {
SPAN_DESCRIPTION,
SPAN_OP,
HTTP_RESPONSE_CONTENT_LENGTH,
SPAN_SELF_TIME,
RESOURCE_RENDER_BLOCKING_STATUS,
} = SpanIndexedField;

export const useIndexedResourcesQuery = () => {
const pageFilters = usePageFilters();
const location = useLocation();
const resourceFilters = useResourceModuleFilters();
const {slug: orgSlug} = useOrganization();
const queryConditions = [
`${SPAN_OP}:resource.img`,
...(resourceFilters['resource.render_blocking_status']
? [
`resource.render_blocking_status:${resourceFilters['resource.render_blocking_status']}`,
]
: [`!resource.render_blocking_status:blocking`]),
];

// TODO - we should be using metrics data here
const eventView = EventView.fromNewQueryWithPageFilters(
{
fields: [
'id',
'project',
'span.group',
'transaction.id',
'count_unique(span.description)',
SPAN_DESCRIPTION,
SPAN_SELF_TIME,
HTTP_RESPONSE_CONTENT_LENGTH,
RESOURCE_RENDER_BLOCKING_STATUS,
],
name: 'Resource module - resource table',
query: queryConditions.join(' '),
version: 2,
orderby: '-count()',
dataset: DiscoverDatasets.SPANS_INDEXED,
},
pageFilters.selection
);

const result = useDiscoverQuery({
eventView,
limit: 100,
location,
orgSlug,
referrer: 'api.performance.browser.resource.image-table',
});

const data =
result?.data?.data.map(row => ({
id: row.id as string,
project: row.project as string,
'transaction.id': row['transaction.id'] as string,
[SPAN_DESCRIPTION]: row[SPAN_DESCRIPTION].toString(),
[SPAN_SELF_TIME]: row[SPAN_SELF_TIME] as number,
[RESOURCE_RENDER_BLOCKING_STATUS]: row[RESOURCE_RENDER_BLOCKING_STATUS] as
| ''
| 'non-blocking'
| 'blocking',
[HTTP_RESPONSE_CONTENT_LENGTH]: row[HTTP_RESPONSE_CONTENT_LENGTH] as number,
})) ?? [];

return {...result, data};
};
Loading
Loading