resourceView.tsx 7.3 KB

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