resourceView.tsx 7.0 KB

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