index.tsx 6.5 KB

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