index.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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 {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import {useLocation} from 'sentry/utils/useLocation';
  8. import {RESOURCE_THROUGHPUT_UNIT} from 'sentry/views/performance/browser/resources';
  9. import ResourceTable from 'sentry/views/performance/browser/resources/jsCssView/resourceTable';
  10. import RenderBlockingSelector from 'sentry/views/performance/browser/resources/shared/renderBlockingSelector';
  11. import SelectControlWithProps from 'sentry/views/performance/browser/resources/shared/selectControlWithProps';
  12. import {
  13. BrowserStarfishFields,
  14. useResourceModuleFilters,
  15. } from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
  16. import {useResourcePagesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcePagesQuery';
  17. import {useResourceSort} from 'sentry/views/performance/browser/resources/utils/useResourceSort';
  18. import {getResourceTypeFilter} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
  19. import {ModuleName} from 'sentry/views/starfish/types';
  20. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  21. import {SpanTimeCharts} from 'sentry/views/starfish/views/spans/spanTimeCharts';
  22. import {ModuleFilters} from 'sentry/views/starfish/views/spans/useModuleFilters';
  23. const {
  24. SPAN_OP: RESOURCE_TYPE,
  25. SPAN_DOMAIN,
  26. TRANSACTION,
  27. RESOURCE_RENDER_BLOCKING_STATUS,
  28. } = BrowserStarfishFields;
  29. export const DEFAULT_RESOURCE_TYPES = [
  30. 'resource.script',
  31. 'resource.css',
  32. 'resource.font',
  33. ];
  34. type Option = {
  35. label: string;
  36. value: string;
  37. };
  38. function JSCSSView() {
  39. const filters = useResourceModuleFilters();
  40. const sort = useResourceSort();
  41. const spanTimeChartsFilters: ModuleFilters = {
  42. 'span.op': `[${DEFAULT_RESOURCE_TYPES.join(',')}]`,
  43. ...(filters[SPAN_DOMAIN] ? {[SPAN_DOMAIN]: filters[SPAN_DOMAIN]} : {}),
  44. };
  45. const extraQuery = getResourceTypeFilter(undefined, DEFAULT_RESOURCE_TYPES);
  46. return (
  47. <Fragment>
  48. <SpanTimeCharts
  49. moduleName={ModuleName.OTHER}
  50. appliedFilters={spanTimeChartsFilters}
  51. throughputUnit={RESOURCE_THROUGHPUT_UNIT}
  52. extraQuery={extraQuery}
  53. />
  54. <FilterOptionsContainer>
  55. <ResourceTypeSelector value={filters[RESOURCE_TYPE] || ''} />
  56. <TransactionSelector
  57. value={filters[TRANSACTION] || ''}
  58. defaultResourceTypes={DEFAULT_RESOURCE_TYPES}
  59. />
  60. <RenderBlockingSelector value={filters[RESOURCE_RENDER_BLOCKING_STATUS] || ''} />
  61. </FilterOptionsContainer>
  62. <ResourceTable sort={sort} defaultResourceTypes={DEFAULT_RESOURCE_TYPES} />
  63. </Fragment>
  64. );
  65. }
  66. function ResourceTypeSelector({value}: {value?: string}) {
  67. const location = useLocation();
  68. const options: Option[] = [
  69. {value: '', label: 'All'},
  70. {value: 'resource.script', label: `${t('JavaScript')} (.js)`},
  71. {value: 'resource.css', label: `${t('Stylesheet')} (.css)`},
  72. {value: 'resource.font', label: `${t('Font')} (.woff, .woff2, .ttf, .otf, .eot)`},
  73. ];
  74. return (
  75. <SelectControlWithProps
  76. inFieldLabel={`${t('Type')}:`}
  77. options={options}
  78. value={value}
  79. onChange={newValue => {
  80. browserHistory.push({
  81. ...location,
  82. query: {
  83. ...location.query,
  84. [RESOURCE_TYPE]: newValue?.value,
  85. [QueryParameterNames.SPANS_CURSOR]: undefined,
  86. },
  87. });
  88. }}
  89. />
  90. );
  91. }
  92. export function TransactionSelector({
  93. value,
  94. defaultResourceTypes,
  95. }: {
  96. defaultResourceTypes?: string[];
  97. value?: string;
  98. }) {
  99. const [state, setState] = useState({
  100. search: '',
  101. inputChanged: false,
  102. shouldRequeryOnInputChange: false,
  103. });
  104. const location = useLocation();
  105. const {data: pages, isLoading} = useResourcePagesQuery(
  106. defaultResourceTypes,
  107. state.search
  108. );
  109. // If the maximum number of pages is returned, we need to requery on input change to get full results
  110. if (!state.shouldRequeryOnInputChange && pages && pages.length >= 100) {
  111. setState({...state, shouldRequeryOnInputChange: true});
  112. }
  113. // Everytime loading is complete, reset the inputChanged state
  114. useEffect(() => {
  115. if (!isLoading && state.inputChanged) {
  116. setState({...state, inputChanged: false});
  117. }
  118. // eslint-disable-next-line react-hooks/exhaustive-deps
  119. }, [isLoading]);
  120. const optionsReady = !isLoading && !state.inputChanged;
  121. const options: Option[] = optionsReady
  122. ? [{value: '', label: 'All'}, ...pages.map(page => ({value: page, label: page}))]
  123. : [];
  124. // eslint-disable-next-line react-hooks/exhaustive-deps
  125. const debounceUpdateSearch = useCallback(
  126. debounce((search, currentState) => {
  127. setState({...currentState, search});
  128. }, 500),
  129. []
  130. );
  131. return (
  132. <SelectControlWithProps
  133. inFieldLabel={`${t('Page')}:`}
  134. options={options}
  135. value={value}
  136. onInputChange={input => {
  137. if (state.shouldRequeryOnInputChange) {
  138. setState({...state, inputChanged: true});
  139. debounceUpdateSearch(input, state);
  140. }
  141. }}
  142. noOptionsMessage={() => (optionsReady ? undefined : t('Loading...'))}
  143. onChange={newValue => {
  144. browserHistory.push({
  145. ...location,
  146. query: {
  147. ...location.query,
  148. [TRANSACTION]: newValue?.value,
  149. [QueryParameterNames.SPANS_CURSOR]: undefined,
  150. },
  151. });
  152. }}
  153. />
  154. );
  155. }
  156. export const FilterOptionsContainer = styled('div')`
  157. display: grid;
  158. grid-template-columns: repeat(4, 1fr);
  159. gap: ${space(2)};
  160. margin-bottom: ${space(2)};
  161. max-width: 800px;
  162. `;
  163. export default JSCSSView;