index.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import {t} from 'sentry/locale';
  5. import {space} from 'sentry/styles/space';
  6. import {trackAnalytics} from 'sentry/utils/analytics';
  7. import {browserHistory} from 'sentry/utils/browserHistory';
  8. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  9. import {useLocation} from 'sentry/utils/useLocation';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import {RESOURCE_THROUGHPUT_UNIT} from 'sentry/views/performance/browser/resources';
  12. import {Referrer} from 'sentry/views/performance/browser/resources/referrer';
  13. import ResourceTable from 'sentry/views/performance/browser/resources/resourceView/resourceTable';
  14. import {
  15. FONT_FILE_EXTENSIONS,
  16. IMAGE_FILE_EXTENSIONS,
  17. } from 'sentry/views/performance/browser/resources/shared/constants';
  18. import RenderBlockingSelector from 'sentry/views/performance/browser/resources/shared/renderBlockingSelector';
  19. import SelectControlWithProps from 'sentry/views/performance/browser/resources/shared/selectControlWithProps';
  20. import {ResourceSpanOps} from 'sentry/views/performance/browser/resources/shared/types';
  21. import {
  22. BrowserStarfishFields,
  23. useResourceModuleFilters,
  24. } from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
  25. import {useResourcePagesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcePagesQuery';
  26. import {useResourceSort} from 'sentry/views/performance/browser/resources/utils/useResourceSort';
  27. import {getResourceTypeFilter} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
  28. import {useHasDataTrackAnalytics} from 'sentry/views/performance/utils/analytics/useHasDataTrackAnalytics';
  29. import {ModuleName} from 'sentry/views/starfish/types';
  30. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  31. import {SpanTimeCharts} from 'sentry/views/starfish/views/spans/spanTimeCharts';
  32. import type {ModuleFilters} from 'sentry/views/starfish/views/spans/useModuleFilters';
  33. const {
  34. SPAN_OP: RESOURCE_TYPE,
  35. SPAN_DOMAIN,
  36. TRANSACTION,
  37. RESOURCE_RENDER_BLOCKING_STATUS,
  38. } = BrowserStarfishFields;
  39. export const DEFAULT_RESOURCE_TYPES = [
  40. ResourceSpanOps.SCRIPT,
  41. ResourceSpanOps.CSS,
  42. ResourceSpanOps.FONT,
  43. ResourceSpanOps.IMAGE,
  44. ];
  45. type Option = {
  46. label: string | React.ReactElement;
  47. value: string;
  48. };
  49. function ResourceView() {
  50. const filters = useResourceModuleFilters();
  51. const sort = useResourceSort();
  52. const spanTimeChartsFilters: ModuleFilters = {
  53. 'span.op': `[${DEFAULT_RESOURCE_TYPES.join(',')}]`,
  54. ...(filters[SPAN_DOMAIN] ? {[SPAN_DOMAIN]: filters[SPAN_DOMAIN]} : {}),
  55. };
  56. const extraQuery = getResourceTypeFilter(undefined, DEFAULT_RESOURCE_TYPES);
  57. useHasDataTrackAnalytics(
  58. MutableSearch.fromQueryObject({
  59. 'span.op': `[${DEFAULT_RESOURCE_TYPES.join(',')}]`,
  60. }),
  61. Referrer.RESOURCE_LANDING,
  62. 'insight.page_loads.assets'
  63. );
  64. return (
  65. <Fragment>
  66. <SpanTimeChartsContainer>
  67. <SpanTimeCharts
  68. moduleName={ModuleName.RESOURCE}
  69. appliedFilters={spanTimeChartsFilters}
  70. throughputUnit={RESOURCE_THROUGHPUT_UNIT}
  71. extraQuery={extraQuery}
  72. />
  73. </SpanTimeChartsContainer>
  74. <FilterOptionsContainer columnCount={3}>
  75. <ResourceTypeSelector value={filters[RESOURCE_TYPE] || ''} />
  76. <TransactionSelector
  77. value={filters[TRANSACTION] || ''}
  78. defaultResourceTypes={DEFAULT_RESOURCE_TYPES}
  79. />
  80. <RenderBlockingSelector value={filters[RESOURCE_RENDER_BLOCKING_STATUS] || ''} />
  81. </FilterOptionsContainer>
  82. <ResourceTable sort={sort} defaultResourceTypes={DEFAULT_RESOURCE_TYPES} />
  83. </Fragment>
  84. );
  85. }
  86. function ResourceTypeSelector({value}: {value?: string}) {
  87. const location = useLocation();
  88. const organization = useOrganization();
  89. const hasImageView = organization.features.includes('insights-initial-modules');
  90. const options: Option[] = [
  91. {value: '', label: 'All'},
  92. {value: 'resource.script', label: `${t('JavaScript')} (.js)`},
  93. {value: 'resource.css', label: `${t('Stylesheet')} (.css)`},
  94. {
  95. value: 'resource.font',
  96. label: `${t('Font')} (${FONT_FILE_EXTENSIONS.map(e => `.${e}`).join(', ')})`,
  97. },
  98. ...(hasImageView
  99. ? [
  100. {
  101. value: ResourceSpanOps.IMAGE,
  102. label: `${t('Image')} (${IMAGE_FILE_EXTENSIONS.map(e => `.${e}`).join(', ')})`,
  103. },
  104. ]
  105. : []),
  106. ];
  107. return (
  108. <SelectControlWithProps
  109. inFieldLabel={`${t('Type')}:`}
  110. options={options}
  111. value={value}
  112. onChange={newValue => {
  113. trackAnalytics('insight.asset.filter_by_type', {
  114. organization,
  115. filter: newValue?.value,
  116. });
  117. browserHistory.push({
  118. ...location,
  119. query: {
  120. ...location.query,
  121. [RESOURCE_TYPE]: newValue?.value,
  122. [QueryParameterNames.SPANS_CURSOR]: undefined,
  123. },
  124. });
  125. }}
  126. />
  127. );
  128. }
  129. export function TransactionSelector({
  130. value,
  131. defaultResourceTypes,
  132. }: {
  133. defaultResourceTypes?: string[];
  134. value?: string;
  135. }) {
  136. const [state, setState] = useState({
  137. search: '',
  138. inputChanged: false,
  139. shouldRequeryOnInputChange: false,
  140. });
  141. const location = useLocation();
  142. const organization = useOrganization();
  143. const {data: pages, isLoading} = useResourcePagesQuery(
  144. defaultResourceTypes,
  145. state.search
  146. );
  147. // If the maximum number of pages is returned, we need to requery on input change to get full results
  148. if (!state.shouldRequeryOnInputChange && pages && pages.length >= 100) {
  149. setState({...state, shouldRequeryOnInputChange: true});
  150. }
  151. // Everytime loading is complete, reset the inputChanged state
  152. useEffect(() => {
  153. if (!isLoading && state.inputChanged) {
  154. setState({...state, inputChanged: false});
  155. }
  156. // eslint-disable-next-line react-hooks/exhaustive-deps
  157. }, [isLoading]);
  158. const optionsReady = !isLoading && !state.inputChanged;
  159. const options: Option[] = optionsReady
  160. ? [{value: '', label: 'All'}, ...pages.map(page => ({value: page, label: page}))]
  161. : [];
  162. // eslint-disable-next-line react-hooks/exhaustive-deps
  163. const debounceUpdateSearch = useCallback(
  164. debounce((search, currentState) => {
  165. setState({...currentState, search});
  166. }, 500),
  167. []
  168. );
  169. return (
  170. <SelectControlWithProps
  171. inFieldLabel={`${t('Page')}:`}
  172. options={options}
  173. value={value}
  174. onInputChange={input => {
  175. if (state.shouldRequeryOnInputChange) {
  176. setState({...state, inputChanged: true});
  177. debounceUpdateSearch(input, state);
  178. }
  179. }}
  180. noOptionsMessage={() => (optionsReady ? undefined : t('Loading...'))}
  181. onChange={newValue => {
  182. trackAnalytics('insight.asset.filter_by_page', {
  183. organization,
  184. });
  185. browserHistory.push({
  186. ...location,
  187. query: {
  188. ...location.query,
  189. [TRANSACTION]: newValue?.value,
  190. [QueryParameterNames.SPANS_CURSOR]: undefined,
  191. },
  192. });
  193. }}
  194. />
  195. );
  196. }
  197. export const SpanTimeChartsContainer = styled('div')`
  198. margin-bottom: ${space(2)};
  199. `;
  200. export const FilterOptionsContainer = styled('div')<{columnCount: number}>`
  201. display: grid;
  202. grid-template-columns: repeat(${props => props.columnCount}, 1fr);
  203. gap: ${space(2)};
  204. margin-bottom: ${space(2)};
  205. max-width: 800px;
  206. `;
  207. export default ResourceView;