index.tsx 6.8 KB

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