index.tsx 5.2 KB

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